Compare commits

..

No commits in common. "43ffcee9c80112730a0d2f50e1defa16f10ef94a" and "211bd5f1158f2fed9db8471b1422bf764bfc9d62" have entirely different histories.

34 changed files with 345 additions and 751 deletions

View File

@ -49,13 +49,6 @@ jobs:
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v5
- name: Configure Gradle properties
run: |
echo 'android.native.buildOutput=verbose' >> ~/.gradle/gradle.properties
echo 'org.gradle.parallel=true' >> ~/.gradle/gradle.properties
echo 'org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 -XX:+UseParallelGC' >> ~/.gradle/gradle.properties
echo 'android.native.buildOutput=verbose' >> ~/.gradle/gradle.properties
- name: Setup ninja
uses: seanmiddleditch/gha-setup-ninja@v6
with:

View File

@ -71,7 +71,7 @@ val verCode by extra(commitCount)
val verName by extra(latestTag)
val androidTargetSdkVersion by extra(36)
val androidMinSdkVersion by extra(27)
val androidBuildToolsVersion by extra("36.1.0")
val androidBuildToolsVersion by extra("36.0.0")
val androidCompileSdkVersion by extra(36)
val androidCompileNdkVersion by extra("29.0.13113456")
val androidSourceCompatibility by extra(JavaVersion.VERSION_21)

View File

@ -22,12 +22,9 @@
-keepclassmembers class org.lsposed.lspd.impl.LSPosedHookCallback {
public <methods>;
}
-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, ***);
-keep,allowoptimization,allowobfuscation @io.github.libxposed.api.annotations.* class * {
@io.github.libxposed.api.annotations.BeforeInvocation <methods>;
@io.github.libxposed.api.annotations.AfterInvocation <methods>;
}
-assumenosideeffects class android.util.Log {
public static *** v(...);

View File

@ -24,7 +24,6 @@ import android.app.ActivityThread;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.util.Log;
import android.util.LogPrinter;
import org.lsposed.lspd.impl.LSPosedBridge;
import org.lsposed.lspd.impl.LSPosedHookCallback;
@ -38,9 +37,7 @@ import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
@ -75,8 +72,6 @@ public final class XposedBridge {
private static final Object[] EMPTY_ARRAY = new Object[0];
private static final SimpleDateFormat format = new SimpleDateFormat("'['yyyy-MM-dd'T'HH:mm:ss.SSS");
// built-in handlers
public static final CopyOnWriteArraySet<XC_LoadPackage> sLoadedPackageCallbacks = new CopyOnWriteArraySet<>();
/*package*/ static final CopyOnWriteArraySet<XC_InitPackageResources> sInitPackageResourcesCallbacks = new CopyOnWriteArraySet<>();
@ -86,12 +81,6 @@ public final class XposedBridge {
public static volatile ClassLoader dummyClassLoader = null;
private static LogPrinter printer;
public static void setLogPrinter(LogPrinter printer){
XposedBridge.printer = printer;
}
public static void initXResources() {
if (dummyClassLoader != null) {
return;
@ -157,9 +146,6 @@ public final class XposedBridge {
*/
public synchronized static void log(String text) {
Log.i(TAG, text);
if (printer != null){
printer.println(format.format(new Date()) + " " + ActivityThread.currentProcessName() + ";" + Thread.currentThread().getName() + "]" + text);
}
}
/**
@ -173,9 +159,6 @@ public final class XposedBridge {
public synchronized static void log(Throwable t) {
String logStr = Log.getStackTraceString(t);
Log.e(TAG, logStr);
if (printer != null){
printer.println(format.format(new Date()) + " " + ActivityThread.currentProcessName() + ";" + Thread.currentThread().getName() + "]" + logStr);
}
}
/**

View File

@ -4,10 +4,14 @@ import android.app.ActivityThread;
import de.robv.android.xposed.XposedInit;
import io.github.libxposed.api.XposedInterface;
import io.github.libxposed.api.annotations.AfterInvocation;
import io.github.libxposed.api.annotations.XposedHooker;
@XposedHooker
public class AttachHooker implements XposedInterface.Hooker {
public static void after(XposedInterface.AfterHookCallback callback) {
@AfterInvocation
public static void afterHookedMethod(XposedInterface.AfterHookCallback callback) {
XposedInit.loadModules((ActivityThread) callback.getThisObject());
}
}

View File

@ -4,10 +4,14 @@ import org.lsposed.lspd.impl.LSPosedBridge;
import org.lsposed.lspd.util.Utils.Log;
import io.github.libxposed.api.XposedInterface;
import io.github.libxposed.api.annotations.BeforeInvocation;
import io.github.libxposed.api.annotations.XposedHooker;
@XposedHooker
public class CrashDumpHooker implements XposedInterface.Hooker {
public static void before(XposedInterface.BeforeHookCallback callback) {
@BeforeInvocation
public static void beforeHookedMethod(XposedInterface.BeforeHookCallback callback) {
try {
var e = (Throwable) callback.getArgs()[0];
LSPosedBridge.log("Crash unexpectedly: " + Log.getStackTraceString(e));

View File

@ -27,8 +27,11 @@ import org.lsposed.lspd.impl.LSPosedHelper;
import org.lsposed.lspd.util.Hookers;
import io.github.libxposed.api.XposedInterface;
import io.github.libxposed.api.annotations.AfterInvocation;
import io.github.libxposed.api.annotations.XposedHooker;
// system_server initialization
@XposedHooker
public class HandleSystemServerProcessHooker implements XposedInterface.Hooker {
public interface Callback {
@ -39,7 +42,8 @@ public class HandleSystemServerProcessHooker implements XposedInterface.Hooker {
public static volatile Callback callback = null;
@SuppressLint("PrivateApi")
public static void after() {
@AfterInvocation
public static void afterHookedMethod() {
Hookers.logD("ZygoteInit#handleSystemServerProcess() starts");
try {
// get system_server classLoader

View File

@ -51,8 +51,11 @@ 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;
import io.github.libxposed.api.annotations.AfterInvocation;
import io.github.libxposed.api.annotations.XposedHooker;
@SuppressLint("BlockedPrivateApi")
@XposedHooker
public class LoadedApkCreateCLHooker implements XposedInterface.Hooker {
private final static Field defaultClassLoaderField;
@ -74,7 +77,8 @@ public class LoadedApkCreateCLHooker implements XposedInterface.Hooker {
loadedApks.add(loadedApk);
}
public static void after(XposedInterface.AfterHookCallback callback) {
@AfterInvocation
public static void afterHookedMethod(XposedInterface.AfterHookCallback callback) {
LoadedApk loadedApk = (LoadedApk) callback.getThisObject();
if (callback.getArgs()[0] != null || !loadedApks.contains(loadedApk)) {
@ -160,31 +164,6 @@ public class LoadedApkCreateCLHooker implements XposedInterface.Hooker {
return isFirstPackage;
}
});
ClassLoader defaultClassLoaderForCompat = classLoader;
if (defaultClassLoaderField != null) {
try {
var defaultCl = (ClassLoader) defaultClassLoaderField.get(loadedApk);
if (defaultCl != null) {
defaultClassLoaderForCompat = defaultCl;
}
} catch (Throwable ignored) {
}
}
Object appComponentFactory = null;
try {
appComponentFactory = XposedHelpers.getObjectField(loadedApk, "mAppComponentFactory");
} catch (Throwable ignored) {
}
LSPosedContext.callOnPackageReady(
loadedApk.getPackageName(),
loadedApk.getApplicationInfo(),
isFirstPackage,
defaultClassLoaderForCompat,
classLoader,
appComponentFactory
);
Hookers.logD("callOnPackageReady via LoadedApkCreateCLHooker: " + loadedApk.getPackageName());
} catch (Throwable t) {
Hookers.logE("error when hooking LoadedApk#createClassLoader", t);
} finally {

View File

@ -29,11 +29,15 @@ 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;
import io.github.libxposed.api.annotations.AfterInvocation;
import io.github.libxposed.api.annotations.XposedHooker;
// when a package is loaded for an existing process, trigger the callbacks as well
@XposedHooker
public class LoadedApkCtorHooker implements XposedInterface.Hooker {
public static void after(XposedInterface.AfterHookCallback callback) {
@AfterInvocation
public static void afterHookedMethod(XposedInterface.AfterHookCallback callback) {
Hookers.logD("LoadedApk#<init> starts");
try {

View File

@ -6,10 +6,14 @@ import org.lsposed.lspd.impl.LSPosedBridge;
import org.lsposed.lspd.nativebridge.HookBridge;
import io.github.libxposed.api.XposedInterface;
import io.github.libxposed.api.annotations.AfterInvocation;
import io.github.libxposed.api.annotations.XposedHooker;
@XposedHooker
public class OpenDexFileHooker implements XposedInterface.Hooker {
public static void after(XposedInterface.AfterHookCallback callback) {
@AfterInvocation
public static void afterHookedMethod(XposedInterface.AfterHookCallback callback) {
ClassLoader classLoader = null;
for (var arg : callback.getArgs()) {
if (arg instanceof ClassLoader) {

View File

@ -32,10 +32,14 @@ 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;
import io.github.libxposed.api.annotations.BeforeInvocation;
import io.github.libxposed.api.annotations.XposedHooker;
@XposedHooker
public class StartBootstrapServicesHooker implements XposedInterface.Hooker {
public static void before() {
@BeforeInvocation
public static void beforeHookedMethod() {
logD("SystemServer#startBootstrapServices() starts");
try {

View File

@ -12,6 +12,9 @@ import java.lang.reflect.Modifier;
import de.robv.android.xposed.XposedBridge;
import io.github.libxposed.api.XposedInterface;
import io.github.libxposed.api.annotations.AfterInvocation;
import io.github.libxposed.api.annotations.BeforeInvocation;
import io.github.libxposed.api.annotations.XposedHooker;
import io.github.libxposed.api.errors.HookFailedError;
public class LSPosedBridge {
@ -215,14 +218,16 @@ public class LSPosedBridge {
throw new IllegalArgumentException("Cannot hook Method.invoke");
} else if (hooker == null) {
throw new IllegalArgumentException("hooker should not be null!");
} else if (hooker.getAnnotation(XposedHooker.class) == null) {
throw new IllegalArgumentException("Hooker should be annotated with @XposedHooker");
}
Method beforeInvocation = null, afterInvocation = null;
var modifiers = Modifier.PUBLIC | Modifier.STATIC;
for (var method : hooker.getDeclaredMethods()) {
if (method.getName().equals("before")) {
if (method.getAnnotation(BeforeInvocation.class) != null) {
if (beforeInvocation != null) {
throw new IllegalArgumentException("More than one method named before");
throw new IllegalArgumentException("More than one method annotated with @BeforeInvocation");
}
boolean valid = (method.getModifiers() & modifiers) == modifiers;
var params = method.getParameterTypes();
@ -232,12 +237,13 @@ public class LSPosedBridge {
valid = false;
}
if (!valid) {
throw new IllegalArgumentException("before method format is invalid");
throw new IllegalArgumentException("BeforeInvocation method format is invalid");
}
beforeInvocation = method;
} else if (method.getName().equals("after")) {
}
if (method.getAnnotation(AfterInvocation.class) != null) {
if (afterInvocation != null) {
throw new IllegalArgumentException("More than one method named after");
throw new IllegalArgumentException("More than one method annotated with @AfterInvocation");
}
boolean valid = (method.getModifiers() & modifiers) == modifiers;
valid &= method.getReturnType().equals(void.class);
@ -248,13 +254,13 @@ public class LSPosedBridge {
valid = false;
}
if (!valid) {
throw new IllegalArgumentException("after method format is invalid");
throw new IllegalArgumentException("AfterInvocation method format is invalid");
}
afterInvocation = method;
}
}
if (beforeInvocation == null && afterInvocation == null) {
throw new IllegalArgumentException("No method named before or after found in " + hooker.getName());
throw new IllegalArgumentException("No method annotated with @BeforeInvocation or @AfterInvocation");
}
try {
if (beforeInvocation == null) {
@ -265,7 +271,7 @@ public class LSPosedBridge {
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");
throw new IllegalArgumentException("BeforeInvocation and AfterInvocation method format is invalid");
}
}
} catch (NoSuchMethodException e) {

View File

@ -28,7 +28,6 @@ import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
@ -76,27 +75,6 @@ public class LSPosedContext implements XposedInterface {
}
}
public static void callOnPackageReady(
String packageName,
ApplicationInfo applicationInfo,
boolean isFirstPackage,
ClassLoader defaultClassLoader,
ClassLoader classLoader,
Object appComponentFactory
) {
var lifecycle = new CompatLifecycleData(
packageName,
applicationInfo,
isFirstPackage,
defaultClassLoader,
classLoader,
appComponentFactory
);
for (XposedModule module : modules) {
invokeCompatLifecycle(module, "onPackageReady", lifecycle);
}
}
public static void callOnSystemServerLoaded(XposedModuleInterface.SystemServerLoadedParam param) {
for (XposedModule module : modules) {
try {
@ -104,18 +82,6 @@ public class LSPosedContext implements XposedInterface {
} catch (Throwable t) {
Log.e(TAG, "Error when calling onSystemServerLoaded of " + module.getApplicationInfo().packageName, t);
}
invokeCompatLifecycle(
module,
"onSystemServerStarting",
new CompatLifecycleData(
null,
null,
false,
param.getClassLoader(),
null,
null
)
);
}
}
@ -146,7 +112,19 @@ public class LSPosedContext implements XposedInterface {
continue;
}
try {
var moduleContext = instantiateModuleCompat(moduleClass, ctx);
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);
@ -161,127 +139,6 @@ public class LSPosedContext implements XposedInterface {
return true;
}
private static XposedModule instantiateModuleCompat(Class<?> moduleClass, LSPosedContext context) throws Throwable {
// New API-style path: no-arg constructor + attachFramework + onModuleLoaded callback.
try {
var ctor = moduleClass.getDeclaredConstructor();
ctor.setAccessible(true);
var instance = ctor.newInstance();
if (instance instanceof XposedModule) {
var typed = (XposedModule) instance;
typed.attachFramework(context);
typed.onModuleLoaded(new XposedModuleInterface.ModuleLoadedParam() {
@Override
public boolean isSystemServer() {
return isSystemServer;
}
@NonNull
@Override
public String getProcessName() {
return processName;
}
});
return (XposedModule) instance;
}
} catch (NoSuchMethodException ignored) {
}
// Old API-style path: constructor takes (XposedInterface, ModuleLoadedParam).
var moduleEntry = moduleClass.getConstructor(XposedInterface.class, XposedModuleInterface.ModuleLoadedParam.class);
return (XposedModule) moduleEntry.newInstance(context, new XposedModuleInterface.ModuleLoadedParam() {
@Override
public boolean isSystemServer() {
return isSystemServer;
}
@NonNull
@Override
public String getProcessName() {
return processName;
}
});
}
private static final class CompatLifecycleData {
final String packageName;
final ApplicationInfo applicationInfo;
final boolean isFirstPackage;
final ClassLoader defaultClassLoader;
final ClassLoader classLoader;
final Object appComponentFactory;
CompatLifecycleData(
String packageName,
ApplicationInfo applicationInfo,
boolean isFirstPackage,
ClassLoader defaultClassLoader,
ClassLoader classLoader,
Object appComponentFactory
) {
this.packageName = packageName;
this.applicationInfo = applicationInfo;
this.isFirstPackage = isFirstPackage;
this.defaultClassLoader = defaultClassLoader;
this.classLoader = classLoader;
this.appComponentFactory = appComponentFactory;
}
}
private static void invokeCompatLifecycle(XposedModule module, String methodName, CompatLifecycleData data) {
try {
for (Method method : module.getClass().getMethods()) {
if (!method.getName().equals(methodName) || method.getParameterCount() != 1) {
continue;
}
var paramType = method.getParameterTypes()[0];
var proxy = Proxy.newProxyInstance(
paramType.getClassLoader(),
new Class<?>[]{paramType},
(proxyObj, invokedMethod, args) -> {
String invokedName = invokedMethod.getName();
return switch (invokedName) {
case "getPackageName" -> data.packageName;
case "getApplicationInfo" -> data.applicationInfo;
case "isFirstPackage" -> data.isFirstPackage;
case "getDefaultClassLoader" -> data.defaultClassLoader;
case "getClassLoader" -> data.classLoader;
case "getAppComponentFactory" -> data.appComponentFactory;
case "getClassLoaderForSystemServer", "getSystemServerClassLoader" -> data.defaultClassLoader;
// Keep Object-contract methods stable for proxy users.
case "toString" -> "CompatLifecycleProxy(" + methodName + "," + data.packageName + ")";
case "hashCode" -> System.identityHashCode(proxyObj);
case "equals" -> proxyObj == (args == null || args.length == 0 ? null : args[0]);
default -> null;
};
}
);
method.invoke(module, proxy);
return;
}
} catch (Throwable t) {
String modulePkg = "<unknown>";
try {
if (module.getApplicationInfo() != null && module.getApplicationInfo().packageName != null) {
modulePkg = module.getApplicationInfo().packageName;
}
} catch (Throwable ignored) {
}
Throwable root = t;
if (t instanceof InvocationTargetException && ((InvocationTargetException) t).getTargetException() != null) {
root = ((InvocationTargetException) t).getTargetException();
} else if (t.getCause() != null) {
root = t.getCause();
}
String brief = "LSP_INVOKE_ERR method=" + methodName
+ " module=" + modulePkg
+ " type=" + root.getClass().getName()
+ " msg=" + String.valueOf(root.getMessage());
android.util.Log.e(TAG, brief);
Log.e(TAG, brief, root);
}
}
@NonNull
@Override
public String getFrameworkName() {
@ -332,27 +189,6 @@ public class LSPosedContext implements XposedInterface {
return LSPosedBridge.doHook(origin, priority, hooker);
}
@Override
@NonNull
public <T> MethodUnhooker<Constructor<T>> hookClassInitializer(@NonNull Class<T> origin, @NonNull Class<? extends Hooker> hooker) {
return hookClassInitializer(origin, PRIORITY_DEFAULT, hooker);
}
@Override
@NonNull
@SuppressWarnings({"unchecked", "rawtypes"})
public <T> MethodUnhooker<Constructor<T>> hookClassInitializer(@NonNull Class<T> origin, int priority, @NonNull Class<? extends Hooker> 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<Method>.
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);
@ -374,16 +210,10 @@ public class LSPosedContext implements XposedInterface {
@Nullable
@Override
public Object invokeOrigin(@NonNull Method method, @Nullable Object thisObject, Object... args) throws InvocationTargetException, IllegalArgumentException, IllegalAccessException {
public Object invokeOrigin(@NonNull Method method, @Nullable Object thisObject, Object[] args) throws InvocationTargetException, IllegalArgumentException, IllegalAccessException {
return HookBridge.invokeOriginalMethod(method, thisObject, args);
}
@Override
public <T> void invokeOrigin(@NonNull Constructor<T> 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';
@ -427,11 +257,6 @@ public class LSPosedContext implements XposedInterface {
return HookBridge.invokeSpecialMethod(method, getExecutableShorty(method), method.getDeclaringClass(), thisObject, args);
}
@Override
public <T> void invokeSpecial(@NonNull Constructor<T> constructor, @NonNull T thisObject, Object... args) throws InvocationTargetException, IllegalArgumentException, IllegalAccessException {
HookBridge.invokeSpecialMethod(constructor, getExecutableShorty(constructor), constructor.getDeclaringClass(), thisObject, args);
}
@NonNull
@Override
public <T> T newInstanceOrigin(@NonNull Constructor<T> constructor, Object... args) throws InvocationTargetException, IllegalAccessException, InstantiationException {

View File

@ -1,7 +1,6 @@
package org.lsposed.lspd.nativebridge;
import java.lang.reflect.Executable;
import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;
import dalvik.annotation.optimization.FastNative;
@ -26,12 +25,4 @@ public class HookBridge {
public static native boolean setTrusted(Object cookie);
public static native Object[][] callbackSnapshot(Class<?> hooker_callback, Executable method);
/**
* Retrieves the static initializer (<clinit>) of a class as a Method object.
* Standard Java reflection cannot access this.
* @param clazz The class to inspect.
* @return A Method object for the static initializer, or null if it doesn't exist.
*/
public static native Method getStaticInitializer(Class<?> clazz);
}

View File

@ -322,21 +322,6 @@ LSP_DEF_NATIVE_METHOD(jobjectArray, HookBridge, callbackSnapshot, jclass callbac
return res;
}
LSP_DEF_NATIVE_METHOD(jobject, HookBridge, getStaticInitializer, jclass target_class) {
// <clinit> is the internal name for a static initializer.
// Its signature is always ()V (no arguments, void return).
jmethodID mid = env->GetStaticMethodID(target_class, "<clinit>", "()V");
if (!mid) {
// If GetStaticMethodID fails, it throws an exception.
// We clear it and return null to let the Java side handle it gracefully.
env->ExceptionClear();
return nullptr;
}
// Convert the method ID to a java.lang.reflect.Method object.
// The last parameter must be JNI_TRUE because it's a static method.
return env->ToReflectedMethod(target_class, mid, JNI_TRUE);
}
static JNINativeMethod gMethods[] = {
LSP_NATIVE_METHOD(HookBridge, hookMethod, "(ZLjava/lang/reflect/Executable;Ljava/lang/Class;ILjava/lang/Object;)Z"),
LSP_NATIVE_METHOD(HookBridge, unhookMethod, "(ZLjava/lang/reflect/Executable;Ljava/lang/Object;)Z"),
@ -347,7 +332,6 @@ static JNINativeMethod gMethods[] = {
LSP_NATIVE_METHOD(HookBridge, instanceOf, "(Ljava/lang/Object;Ljava/lang/Class;)Z"),
LSP_NATIVE_METHOD(HookBridge, setTrusted, "(Ljava/lang/Object;)Z"),
LSP_NATIVE_METHOD(HookBridge, callbackSnapshot, "(Ljava/lang/Class;Ljava/lang/reflect/Executable;)[[Ljava/lang/Object;"),
LSP_NATIVE_METHOD(HookBridge, getStaticInitializer, "(Ljava/lang/Class;)Ljava/lang/reflect/Method;"),
};
void RegisterHookBridge(JNIEnv *env) {

View File

@ -193,18 +193,9 @@ public class ConfigFileManager {
public static boolean chattr0(Path path) {
try {
var dir = Os.open(path.toString(), OsConstants.O_RDONLY, 0);
// Clear all special file attributes on the directory
HiddenApiBridge.Os_ioctlInt(dir, Process.is64Bit() ? 0x40086602 : 0x40046602, 0);
Os.close(dir);
return true;
} catch (ErrnoException e) {
// If the operation is not supported (ENOTSUP), it means the filesystem doesn't support attributes.
// We can assume the file is not immutable and proceed.
if (e.errno == OsConstants.ENOTSUP) {
return true;
}
Log.d(TAG, "chattr 0", e);
return false;
} catch (Throwable e) {
Log.d(TAG, "chattr 0", e);
return false;

View File

@ -40,7 +40,6 @@ import androidx.annotation.RequiresApi;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
@ -68,74 +67,15 @@ public class Dex2OatService implements Runnable {
}
}
/**
* Checks the ELF header of the target file.
* If 32-bit -> Assigns to Index 0 (Release) or 1 (Debug).
* If 64-bit -> Assigns to Index 2 (Release) or 3 (Debug).
*/
private void checkAndAddDex2Oat(String path) {
if (path == null)
return;
File file = new File(path);
if (!file.exists())
return;
try (FileInputStream fis = new FileInputStream(file)) {
byte[] header = new byte[5];
if (fis.read(header) != 5)
return;
// 1. Verify ELF Magic: 0x7F 'E' 'L' 'F'
if (header[0] != 0x7F || header[1] != 'E' || header[2] != 'L' || header[3] != 'F') {
return;
}
// 2. Check Architecture (header[4]): 1 = 32-bit, 2 = 64-bit
boolean is32Bit = (header[4] == 1);
boolean is64Bit = (header[4] == 2);
boolean isDebug = path.contains("dex2oatd");
int index = -1;
if (is32Bit) {
index = isDebug ? 1 : 0; // Index 0/1 maps to r32/d32 in C++
} else if (is64Bit) {
index = isDebug ? 3 : 2; // Index 2/3 maps to r64/d64 in C++
}
// 3. Assign to the detected slot
if (index != -1 && dex2oatArray[index] == null) {
dex2oatArray[index] = path;
try {
// Open the FD for the wrapper to use later
fdArray[index] = Os.open(path, OsConstants.O_RDONLY, 0);
Log.i(TAG, "Detected " + path + " as " + (is64Bit ? "64-bit" : "32-bit") + " -> Assigned Index "
+ index);
} catch (ErrnoException e) {
Log.e(TAG, "Failed to open FD for " + path, e);
dex2oatArray[index] = null;
}
}
} catch (IOException e) {
// File not readable, skip
}
}
public Dex2OatService() {
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
// Android 10: Check the standard path.
// Logic will detect if it is 32-bit and put it in Index 0.
checkAndAddDex2Oat("/apex/com.android.runtime/bin/dex2oat");
checkAndAddDex2Oat("/apex/com.android.runtime/bin/dex2oatd");
// Check for explicit 64-bit paths (just in case)
checkAndAddDex2Oat("/apex/com.android.runtime/bin/dex2oat64");
checkAndAddDex2Oat("/apex/com.android.runtime/bin/dex2oatd64");
openDex2oat(Process.is64Bit() ? 2 : 0, "/apex/com.android.runtime/bin/dex2oat");
openDex2oat(Process.is64Bit() ? 3 : 1, "/apex/com.android.runtime/bin/dex2oatd");
} else {
checkAndAddDex2Oat("/apex/com.android.art/bin/dex2oat32");
checkAndAddDex2Oat("/apex/com.android.art/bin/dex2oatd32");
checkAndAddDex2Oat("/apex/com.android.art/bin/dex2oat64");
checkAndAddDex2Oat("/apex/com.android.art/bin/dex2oatd64");
openDex2oat(0, "/apex/com.android.art/bin/dex2oat32");
openDex2oat(1, "/apex/com.android.art/bin/dex2oatd32");
openDex2oat(2, "/apex/com.android.art/bin/dex2oat64");
openDex2oat(3, "/apex/com.android.art/bin/dex2oatd64");
}
openDex2oat(4, "/data/adb/modules/zygisk_lsposed/bin/liboat_hook32.so");

View File

@ -268,7 +268,7 @@ public class LSPManagerService extends ILSPManagerService.Stub {
@Override
public int getXposedApiVersion() {
return IXposedService.LIB_API;
return IXposedService.API;
}
@Override

View File

@ -125,7 +125,7 @@ public class LSPModuleService extends IXposedService.Stub {
@Override
public int getAPIVersion() throws RemoteException {
ensureModule();
return IXposedService.LIB_API;
return API;
}
@Override
@ -152,12 +152,6 @@ public class LSPModuleService extends IXposedService.Stub {
return IXposedService.FRAMEWORK_PRIVILEGE_ROOT;
}
@Override
public long getFrameworkProperties() throws RemoteException {
ensureModule();
return IXposedService.PROP_CAP_SYSTEM | IXposedService.PROP_CAP_REMOTE;
}
@Override
public List<String> getScope() throws RemoteException {
ensureModule();

View File

@ -229,8 +229,7 @@ void Logcat::ProcessBuffer(struct log_msg *buf) {
tag == "APatchD"sv || tag == "Dobby"sv || tag.starts_with("dex2oat"sv) ||
tag == "KernelSU"sv || tag == "LSPlant"sv || tag == "LSPlt"sv ||
tag.starts_with("LSPosed"sv) || tag == "Magisk"sv || tag == "SELinux"sv ||
tag == "TEESimulator"sv || tag.starts_with("Vector"sv) ||
tag.starts_with("zygisk"sv))) [[unlikely]] {
tag == "TEESimulator"sv || tag.starts_with("zygisk"sv))) [[unlikely]] {
verbose_print_count_ += PrintLogLine(entry, verbose_file_.get());
}
if (entry.pid == my_pid_ && tag == "LSPosedLogcat"sv) [[unlikely]] {

View File

@ -1,37 +0,0 @@
# VectorDex2Oat
VectorDex2Oat is a specialized wrapper and instrumentation suite for the Android `dex2oat` (Ahead-of-Time compiler) binary. It is designed to intercept the compilation process, force specific compiler behaviors (specifically disabling method inlining), and transparently spoof the resulting OAT metadata to hide the presence of the wrapper.
## Overview
In the Android Runtime (ART), `dex2oat` compiles DEX files into OAT files. Modern ART optimizations often inline methods, making it difficult for instrumentation tools to hook specific function calls.
This project consists of two primary components:
1. **dex2oat (Wrapper):** A replacement binary that intercepts the execution, communicates via Unix Domain Sockets to obtain the original compiler binary, and executes it with forced flags.
2. **liboat_hook.so (Hooker):** A shared library injected into the `dex2oat` process via `LD_PRELOAD` that utilizes PLT hooking to sanitize the OAT header's command-line metadata.
## Key Features
* **Inlining Suppression:** Appends `--inline-max-code-units=0` to the compiler arguments, ensuring all methods remain discrete and hookable.
* **FD-Based Execution:** Executes the original `dex2oat` via the system linker using `/proc/self/fd/` paths, avoiding direct execution of files on the disk.
* **Metadata Spoofing:** Intercepts `art::OatHeader::ComputeChecksum` or `art::OatHeader::GetKeyValueStore` to remove traces of the wrapper and its injected flags from the final `.oat` file.
* **Abstract Socket Communication:** Uses the Linux Abstract Namespace for Unix sockets to coordinate file descriptor passing between the controller and the wrapper.
## Architecture
### The Wrapper [dex2oat.cpp](src/main/cpp/dex2oat.cpp)
The wrapper acts as a "man-in-the-middle" for the compiler. When called by the system, it
1. connects to a predefined Unix socket (the stub name `5291374ceda0...` will be replaced during installation of `Vector`);
2. identifies the target architecture (32-bit vs 64-bit) and debug status;
3. receives File Descriptors (FDs) for both the original `dex2oat` binary and the `oat_hook` library;
4. reconstructs the command line, replacing the wrapper path with the original binary path and appending the "no-inline" flags;
5. clears `LD_LIBRARY_PATH` and sets `LD_PRELOAD` to the hooker library's FD;
6. invokes the dynamic linker (`linker64`) to execute the compiler.
### The Hooker [oat_hook.cpp](src/main/cpp/oat_hook.cpp)
The hooker library is preloaded into the compiler's address space. It uses the [LSPlt](https://github.com/JingMatrix/LSPlt) library to:
1. Scan the memory map to find the `dex2oat` binary.
2. Locate and hook internal ART functions:
* [art::OatHeader::GetKeyValueStore](https://cs.android.com/android/platform/superproject/+/android-latest-release:art/runtime/oat/oat.cc;l=366)
* [art::OatHeader::ComputeChecksum](https://cs.android.com/android/platform/superproject/+/android-latest-release:art/runtime/oat/oat.cc;l=366)
3. When the compiler attempts to write the "dex2oat-cmdline" key into the OAT header, the hooker intercepts the call, parses the key-value store, and removes the wrapper-specific flags and paths.

View File

@ -1,15 +1,47 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*
* Copyright (C) 2022 LSPosed Contributors
*/
plugins {
alias(libs.plugins.agp.lib)
}
android {
namespace = "org.matrix.vector.dex2oat"
namespace = "org.lsposed.dex2oat"
androidResources.enable = false
buildFeatures {
androidResources = false
buildConfig = false
prefab = true
prefabPublishing = true
}
defaultConfig {
minSdk = 29
}
externalNativeBuild {
cmake {
path("src/main/cpp/CMakeLists.txt")
}
}
prefab {
register("dex2oat")
}
}

View File

@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.10)
project(dex2oat)
add_executable(dex2oat dex2oat.cpp)
add_library(oat_hook SHARED oat_hook.cpp)
add_library(oat_hook SHARED oat_hook.cpp oat.cpp)
OPTION(LSPLT_BUILD_SHARED OFF)
add_subdirectory(${EXTERNAL_ROOT}/lsplt/lsplt/src/main/jni external)

View File

@ -1,195 +1,148 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*
* Copyright (C) 2022 LSPosed Contributors
*/
//
// Created by Nullptr on 2022/4/1.
//
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>
#include "logging.h"
// Access to the process environment variables
extern "C" char **environ;
#if defined(__LP64__)
#define LP_SELECT(lp32, lp64) lp64
#else
#define LP_SELECT(lp32, lp64) lp32
#endif
namespace {
#define ID_VEC(is64, is_debug) (((is64) << 1) | (is_debug))
constexpr char kSockName[] = "5291374ceda0aef7c5d86cd2a4f6a3ac";
const char kSockName[] = "5291374ceda0aef7c5d86cd2a4f6a3ac\0";
/**
* Calculates a vector ID based on architecture and debug status.
*/
inline int get_id_vec(bool is64, bool is_debug) {
return (static_cast<int>(is64) << 1) | static_cast<int>(is_debug);
}
/**
* Wraps recvmsg with error logging.
*/
ssize_t xrecvmsg(int sockfd, struct msghdr *msg, int flags) {
ssize_t rec = recvmsg(sockfd, msg, flags);
static ssize_t xrecvmsg(int sockfd, struct msghdr *msg, int flags) {
int rec = recvmsg(sockfd, msg, flags);
if (rec < 0) {
PLOGE("recvmsg");
}
return rec;
}
/**
* Receives file descriptors passed over a Unix domain socket using SCM_RIGHTS.
*
* @return Pointer to the FD data on success, nullptr on failure.
*/
void *recv_fds(int sockfd, char *cmsgbuf, size_t bufsz, int cnt) {
static void *recv_fds(int sockfd, char *cmsgbuf, size_t bufsz, int cnt) {
struct iovec iov = {
.iov_base = &cnt,
.iov_len = sizeof(cnt),
};
struct msghdr msg = {.msg_name = nullptr,
.msg_namelen = 0,
.msg_iov = &iov,
.msg_iovlen = 1,
.msg_control = cmsgbuf,
.msg_controllen = bufsz,
.msg_flags = 0};
if (xrecvmsg(sockfd, &msg, MSG_WAITALL) < 0) return nullptr;
struct msghdr msg = {
.msg_iov = &iov, .msg_iovlen = 1, .msg_control = cmsgbuf, .msg_controllen = bufsz};
xrecvmsg(sockfd, &msg, MSG_WAITALL);
struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
if (msg.msg_controllen != bufsz || cmsg == nullptr ||
if (msg.msg_controllen != bufsz || cmsg == NULL ||
cmsg->cmsg_len != CMSG_LEN(sizeof(int) * cnt) || cmsg->cmsg_level != SOL_SOCKET ||
cmsg->cmsg_type != SCM_RIGHTS) {
return nullptr;
return NULL;
}
return CMSG_DATA(cmsg);
}
/**
* Helper to receive a single FD from the socket.
*/
int recv_fd(int sockfd) {
static int recv_fd(int sockfd) {
char cmsgbuf[CMSG_SPACE(sizeof(int))];
void *data = recv_fds(sockfd, cmsgbuf, sizeof(cmsgbuf), 1);
if (data == nullptr) return -1;
if (data == NULL) return -1;
int result;
std::memcpy(&result, data, sizeof(int));
memcpy(&result, data, sizeof(int));
return result;
}
/**
* Reads an integer acknowledgment from the socket.
*/
int read_int(int fd) {
static int read_int(int fd) {
int val;
if (read(fd, &val, sizeof(val)) != sizeof(val)) return -1;
return val;
}
/**
* Writes an integer command/ID to the socket.
*/
void write_int(int fd, int val) {
static void write_int(int fd, int val) {
if (fd < 0) return;
(void)write(fd, &val, sizeof(val));
write(fd, &val, sizeof(val));
}
} // namespace
int main(int argc, char **argv) {
LOGD("dex2oat wrapper ppid=%d", getppid());
// Prepare Unix domain socket address (Abstract Namespace)
struct sockaddr_un sock = {};
sock.sun_family = AF_UNIX;
// sock.sun_path[0] is already \0, so we copy name into sun_path + 1
std::strncpy(sock.sun_path + 1, kSockName, sizeof(sock.sun_path) - 2);
strlcpy(sock.sun_path + 1, kSockName, sizeof(sock.sun_path) - 1);
// Abstract socket length: family + leading \0 + string length
socklen_t len = sizeof(sock.sun_family) + strlen(kSockName) + 1;
// 1. Get original dex2oat binary FD
int sock_fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (connect(sock_fd, reinterpret_cast<struct sockaddr *>(&sock), len)) {
size_t len = sizeof(sa_family_t) + strlen(sock.sun_path + 1) + 1;
if (connect(sock_fd, (struct sockaddr *)&sock, len)) {
PLOGE("failed to connect to %s", sock.sun_path + 1);
return 1;
}
bool is_debug = (argv[0] != nullptr && std::strstr(argv[0], "dex2oatd") != nullptr);
write_int(sock_fd, get_id_vec(LP_SELECT(false, true), is_debug));
write_int(sock_fd, ID_VEC(LP_SELECT(0, 1), strstr(argv[0], "dex2oatd") != NULL));
int stock_fd = recv_fd(sock_fd);
read_int(sock_fd); // Sync
read_int(sock_fd);
close(sock_fd);
// 2. Get liboat_hook.so FD
sock_fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (connect(sock_fd, reinterpret_cast<struct sockaddr *>(&sock), len)) {
if (connect(sock_fd, (struct sockaddr *)&sock, len)) {
PLOGE("failed to connect to %s", sock.sun_path + 1);
return 1;
}
write_int(sock_fd, LP_SELECT(4, 5));
int hooker_fd = recv_fd(sock_fd);
read_int(sock_fd); // Sync
read_int(sock_fd);
close(sock_fd);
if (hooker_fd == -1) {
LOGE("failed to read liboat_hook.so");
PLOGE("failed to read liboat_hook.so");
}
LOGD("sock: %s stock_fd: %d", sock.sun_path + 1, stock_fd);
LOGD("sock: %s %d", sock.sun_path + 1, stock_fd);
// Prepare arguments for execve
// Logic: [linker] [/proc/self/fd/stock_fd] [original_args...] [--inline-max-code-units=0]
std::vector<const char *> exec_argv;
const char *new_argv[argc + 2];
for (int i = 0; i < argc; i++) new_argv[i] = argv[i];
new_argv[argc] = "--inline-max-code-units=0";
new_argv[argc + 1] = NULL;
const char *linker_path =
LP_SELECT("/apex/com.android.runtime/bin/linker", "/apex/com.android.runtime/bin/linker64");
char stock_fd_path[64];
std::snprintf(stock_fd_path, sizeof(stock_fd_path), "/proc/self/fd/%d", stock_fd);
exec_argv.push_back(linker_path);
exec_argv.push_back(stock_fd_path);
// Append original arguments starting from argv[1]
for (int i = 1; i < argc; ++i) {
exec_argv.push_back(argv[i]);
if (getenv("LD_LIBRARY_PATH") == NULL) {
char const *libenv = LP_SELECT(
"LD_LIBRARY_PATH=/apex/com.android.art/lib:/apex/com.android.os.statsd/lib",
"LD_LIBRARY_PATH=/apex/com.android.art/lib64:/apex/com.android.os.statsd/lib64");
putenv((char *)libenv);
}
// Append hooking flags to disable inline, which is our purpose of this wrapper, since we cannot
// hook inlined target methods.
exec_argv.push_back("--inline-max-code-units=0");
exec_argv.push_back(nullptr);
// Set LD_PRELOAD to load liboat_hook.so
const int STRING_BUFFER = 50;
char env_str[STRING_BUFFER];
snprintf(env_str, STRING_BUFFER, "LD_PRELOAD=/proc/%d/fd/%d", getpid(), hooker_fd);
putenv(env_str);
LOGD("Set env %s", env_str);
// Setup Environment variables
// Clear LD_LIBRARY_PATH to let the linker use internal config
unsetenv("LD_LIBRARY_PATH");
fexecve(stock_fd, (char **)new_argv, environ);
// Set LD_PRELOAD to point to the hooker library FD
std::string preload_val = "LD_PRELOAD=/proc/self/fd/" + std::to_string(hooker_fd);
setenv("LD_PRELOAD", ("/proc/self/fd/" + std::to_string(hooker_fd)).c_str(), 1);
// Pass original argv[0] as DEX2OAT_CMD
if (argv[0]) {
setenv("DEX2OAT_CMD", argv[0], 1);
LOGD("DEX2OAT_CMD set to %s", argv[0]);
}
LOGI("Executing via linker: %s executing %s", linker_path, stock_fd_path);
// Perform the execution
execve(linker_path, const_cast<char *const *>(exec_argv.data()), environ);
// If we reach here, execve failed
PLOGE("execve failed");
PLOGE("fexecve failed");
return 2;
}

View File

@ -1,10 +1,10 @@
#pragma once
#include <android/log.h>
#include <errno.h>
#include <android/log.h>
#ifndef LOG_TAG
#define LOG_TAG "VectorDex2Oat"
#define LOG_TAG "LSPosedDex2Oat"
#endif
#ifdef LOG_DISABLED
@ -15,7 +15,11 @@
#define LOGE(...) 0
#else
#ifndef NDEBUG
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGD(fmt, ...) \
__android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, \
"%s:%d#%s" \
": " fmt, \
__FILE_NAME__, __LINE__, __PRETTY_FUNCTION__ __VA_OPT__(, ) __VA_ARGS__)
#define LOGV(fmt, ...) \
__android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, \
"%s:%d#%s" \

View File

@ -72,9 +72,17 @@ public:
static constexpr const char kTrueValue[] = "true";
static constexpr const char kFalseValue[] = "false";
// Added helper to access the key_value_store_ field, which could be fragile across
// different Android versions and compiler optimizations.
const uint8_t* getKeyValueStore() const { return key_value_store_; }
static constexpr size_t Get_key_value_store_size_Offset() {
return offsetof(OatHeader, key_value_store_size_);
}
static constexpr size_t Get_key_value_store_Offset() {
return offsetof(OatHeader, key_value_store_);
}
uint32_t GetKeyValueStoreSize() const;
const uint8_t* GetKeyValueStore() const;
void SetKeyValueStoreSize(uint32_t new_size);
void ComputeChecksum(/*inout*/ uint32_t* checksum) const;

View File

@ -0,0 +1,17 @@
#include "oat.h"
namespace art {
uint32_t OatHeader::GetKeyValueStoreSize() const {
return *(uint32_t*)((uintptr_t)this + OatHeader::Get_key_value_store_size_Offset());
}
const uint8_t* OatHeader::GetKeyValueStore() const {
return (const uint8_t*)((uintptr_t)this + OatHeader::Get_key_value_store_Offset());
}
void OatHeader::SetKeyValueStoreSize(uint32_t new_size) {
*reinterpret_cast<uint32_t*>((uintptr_t)this + Get_key_value_store_size_Offset()) = new_size;
}
} // namespace art

View File

@ -1,206 +1,122 @@
#include <dlfcn.h>
#include <algorithm>
#include <cinttypes>
#include <cstdint>
#include <cstring>
#include <lsplt.hpp>
#include <map>
#include <string>
#include <string_view>
#include <vector>
#include "logging.h"
#include "oat.h"
/**
* This library is injected into dex2oat to intercept the generation of OAT headers. Our wrapper
* runs dex2oat via the linker with extra flags. Without this hook, the resulting OAT file would
* record the transferred fd path of wrapper and the extra flags in its "dex2oat-cmdline" key, which
* can be used to detect the wrapper.
*/
const std::string_view param_to_remove = " --inline-max-code-units=0";
namespace {
const std::string_view kParamToRemove = "--inline-max-code-units=0";
std::string g_binary_path = getenv("DEX2OAT_CMD"); // The original binary path
} // namespace
#define DCL_HOOK_FUNC(ret, func, ...) \
ret (*old_##func)(__VA_ARGS__); \
ret new_##func(__VA_ARGS__)
/**
* Sanitizes the command line string by:
* 1. Replacing the first token (the linker/binary path) with the original dex2oat path.
* 2. Removing the specific optimization flag we injected.
*/
std::string process_cmd(std::string_view sv, std::string_view new_cmd_path) {
std::vector<std::string> tokens;
std::string current;
bool store_resized = false;
// Simple split by space
for (char c : sv) {
if (c == ' ') {
if (!current.empty()) {
tokens.push_back(std::move(current));
current.clear();
}
} else {
current.push_back(c);
}
}
if (!current.empty()) tokens.push_back(std::move(current));
// 1. Replace the command path (argv[0])
if (!tokens.empty()) {
tokens[0] = std::string(new_cmd_path);
}
// 2. Remove the injected parameter if it exists
auto it = std::remove(tokens.begin(), tokens.end(), std::string(kParamToRemove));
tokens.erase(it, tokens.end());
// 3. Join tokens back into a single string
std::string result;
for (size_t i = 0; i < tokens.size(); ++i) {
result += tokens[i];
if (i != tokens.size() - 1) result += ' ';
}
return result;
}
/**
* Re-serializes the Key-Value map back into the OAT header memory space.
*/
uint8_t* WriteKeyValueStore(const std::map<std::string, std::string>& key_values, uint8_t* store) {
LOGD("Writing KeyValueStore back to memory");
char* data_ptr = reinterpret_cast<char*>(store);
for (const auto& [key, value] : key_values) {
// Copy key + null terminator
std::memcpy(data_ptr, key.c_str(), key.length() + 1);
data_ptr += key.length() + 1;
// Copy value + null terminator
std::memcpy(data_ptr, value.c_str(), value.length() + 1);
data_ptr += value.length() + 1;
}
LOGD("Written KeyValueStore with size: %zu", reinterpret_cast<uint8_t*>(data_ptr) - store);
return reinterpret_cast<uint8_t*>(data_ptr);
}
// Helper function to test if a header field could have variable length
bool IsNonDeterministic(const std::string_view& key) {
auto variable_fields = art::OatHeader::kNonDeterministicFieldsAndLengths;
return std::any_of(variable_fields.begin(), variable_fields.end(),
[&key](const auto& pair) { return pair.first.compare(key) == 0; });
}
/**
* Parses the OAT KeyValueStore and spoofs the "dex2oat-cmdline" entry.
*
* @return true if the store was modified in-place or successfully rebuilt.
*/
bool SpoofKeyValueStore(uint8_t* store) {
if (!store) return false;
uint32_t* const store_size_ptr = reinterpret_cast<uint32_t*>(store - sizeof(uint32_t));
uint32_t const store_size = *store_size_ptr;
const char* ptr = reinterpret_cast<const char*>(store);
const char* const store_end = ptr + store_size;
std::map<std::string, std::string> new_store_map;
LOGI("Parsing KeyValueStore [%p - %p] of size %u", ptr, store_end, store_size);
bool store_modified = false;
while (ptr < store_end && *ptr != '\0') {
// Find key
const char* key_end = reinterpret_cast<const char*>(std::memchr(ptr, 0, store_end - ptr));
if (!key_end) break;
std::string_view key(ptr, key_end - ptr);
// Find value
const char* value_start = key_end + 1;
if (value_start >= store_end) break;
const char* value_end =
reinterpret_cast<const char*>(std::memchr(value_start, 0, store_end - value_start));
if (!value_end) break;
std::string_view value(value_start, value_end - value_start);
const bool has_padding =
value_end + 1 < store_end && *(value_end + 1) == '\0' && IsNonDeterministic(key);
if (key == art::OatHeader::kDex2OatCmdLineKey &&
value.find(kParamToRemove) != std::string_view::npos) {
std::string cleaned_cmd = process_cmd(value, g_binary_path);
LOGI("Spoofing cmdline: Original size %zu -> New size %zu", value.length(),
cleaned_cmd.length());
// We can overwrite in-place if the padding is enabled
if (has_padding) {
LOGI("In-place spoofing dex2oat-cmdline (padding detected)");
// Zero out the entire original value range to be safe
size_t original_capacity = value.length();
std::memset(const_cast<char*>(value_start), 0, original_capacity);
// Write the new command.
std::memcpy(const_cast<char*>(value_start), cleaned_cmd.c_str(),
std::min(cleaned_cmd.length(), original_capacity));
return true;
}
// Standard logic: store in map and rebuild later
new_store_map[std::string(key)] = std::move(cleaned_cmd);
store_modified = true;
} else {
new_store_map[std::string(key)] = std::string(value);
LOGI("Parsed item:\t[%s:%s]", key.data(), value.data());
}
ptr = value_end + 1;
if (has_padding) {
while (*ptr == '\0') {
ptr++;
}
}
}
if (store_modified) {
uint8_t* const new_store_end = WriteKeyValueStore(new_store_map, store);
*store_size_ptr = new_store_end - store;
LOGI("Store size set to %u", *store_size_ptr);
return true;
}
bool ModifyStoreInPlace(uint8_t* store, uint32_t store_size) {
if (store == nullptr || store_size == 0) {
return false;
}
#define DCL_HOOK_FUNC(ret, func, ...) \
ret (*old_##func)(__VA_ARGS__) = nullptr; \
ret new_##func(__VA_ARGS__)
// Define the search space
uint8_t* const store_begin = store;
uint8_t* const store_end = store + store_size;
// For Android version < 16
DCL_HOOK_FUNC(uint8_t*, _ZNK3art9OatHeader16GetKeyValueStoreEv, void* header) {
uint8_t* const key_value_store = old__ZNK3art9OatHeader16GetKeyValueStoreEv(header);
// 1. Search for the parameter in the memory buffer
auto it = std::search(store_begin, store_end, param_to_remove.begin(), param_to_remove.end());
SpoofKeyValueStore(key_value_store);
return key_value_store;
// Check if the parameter was found
if (it == store_end) {
LOGD("Parameter '%.*s' not found.", (int)param_to_remove.size(), param_to_remove.data());
return false;
}
uint8_t* location_of_param = it;
LOGD("Parameter found at offset %td.", location_of_param - store_begin);
// 2. Check if there is padding immediately after the string
uint8_t* const byte_after_param = location_of_param + param_to_remove.size();
bool has_padding = false;
// Boundary check: ensure the byte after the parameter is within the buffer
if (byte_after_param + 1 < store_end) {
if (*(byte_after_param + 1) == '\0') {
has_padding = true;
}
}
// 3. Perform the conditional action
if (has_padding) {
// CASE A: Padding exists. Overwrite the parameter with zeros.
LOGD("Padding found. Overwriting parameter with zeros.");
memset(location_of_param, 0, param_to_remove.size());
return false; // Size did not change
} else {
// CASE B: No padding exists (or parameter is at the very end).
// Remove the parameter by shifting the rest of the memory forward.
LOGD("No padding found. Removing parameter and shifting memory.");
// Calculate what to move
uint8_t* source = byte_after_param;
uint8_t* destination = location_of_param;
size_t bytes_to_move = store_end - source;
// memmove is required because the source and destination buffers overlap
if (bytes_to_move > 0) {
memmove(destination, source, bytes_to_move);
}
// 4. Update the total size of the store
store_size -= param_to_remove.size();
LOGD("Store size changed. New size: %u", store_size);
return true; // Size changed
}
}
DCL_HOOK_FUNC(uint32_t, _ZNK3art9OatHeader20GetKeyValueStoreSizeEv, void* header) {
uint32_t size = old__ZNK3art9OatHeader20GetKeyValueStoreSizeEv(header);
if (store_resized) {
LOGD("OatHeader::GetKeyValueStoreSize() called on object at %p\n", header);
size = size - param_to_remove.size();
}
return size;
}
DCL_HOOK_FUNC(uint8_t*, _ZNK3art9OatHeader16GetKeyValueStoreEv, void* header) {
LOGD("OatHeader::GetKeyValueStore() called on object at %p\n", header);
uint8_t* key_value_store_ = old__ZNK3art9OatHeader16GetKeyValueStoreEv(header);
uint32_t key_value_store_size_ = old__ZNK3art9OatHeader20GetKeyValueStoreSizeEv(header);
LOGD("KeyValueStore via hook: [addr: %p, size: %u]", key_value_store_, key_value_store_size_);
store_resized = ModifyStoreInPlace(key_value_store_, key_value_store_size_);
return key_value_store_;
}
// For Android version 16+ : Intercept during checksum calculation
DCL_HOOK_FUNC(void, _ZNK3art9OatHeader15ComputeChecksumEPj, void* header, uint32_t* checksum) {
auto* oat_header = reinterpret_cast<art::OatHeader*>(header);
uint8_t* const store = const_cast<uint8_t*>(oat_header->getKeyValueStore());
SpoofKeyValueStore(store);
// Call original to compute checksum on our modified data
art::OatHeader* oat_header = reinterpret_cast<art::OatHeader*>(header);
const uint8_t* key_value_store_ = oat_header->GetKeyValueStore();
uint32_t key_value_store_size_ = oat_header->GetKeyValueStoreSize();
LOGD("KeyValueStore via offset: [addr: %p, size: %u]", key_value_store_, key_value_store_size_);
store_resized =
ModifyStoreInPlace(const_cast<uint8_t*>(key_value_store_), key_value_store_size_);
if (store_resized) {
oat_header->SetKeyValueStoreSize(key_value_store_size_ - param_to_remove.size());
}
old__ZNK3art9OatHeader15ComputeChecksumEPj(header, checksum);
LOGV("OAT Checksum recalculated: 0x%08X", *checksum);
LOGD("ComputeChecksum called: %" PRIu32, *checksum);
}
#undef DCL_HOOK_FUNC
void register_hook(dev_t dev, ino_t inode, const char* symbol, void* new_func, void** old_func) {
LOGD("RegisterHook: %s, %p, %p", symbol, new_func, old_func);
if (!lsplt::RegisterHook(dev, inode, symbol, new_func, old_func)) {
LOGE("Failed to register PLT hook: %s", symbol);
LOGE("Failed to register plt_hook \"%s\"\n", symbol);
}
}
@ -213,28 +129,16 @@ void register_hook(dev_t dev, ino_t inode, const char* symbol, void* new_func, v
__attribute__((constructor)) static void initialize() {
dev_t dev = 0;
ino_t inode = 0;
// Locate the dex2oat binary in memory to get its device and inode for PLT hooking
for (const auto& info : lsplt::MapInfo::Scan()) {
if (info.path.find("bin/dex2oat") != std::string::npos) {
for (auto& info : lsplt::MapInfo::Scan()) {
if (info.path.starts_with("/apex/com.android.art/bin/dex2oat")) {
dev = info.dev;
inode = info.inode;
if (g_binary_path.empty()) g_binary_path = std::string(info.path);
LOGD("Found target: %s (dev: %ju, inode: %ju)", info.path.data(), (uintmax_t)dev,
(uintmax_t)inode);
break;
}
}
if (dev == 0) {
LOGE("Could not locate dex2oat memory map");
return;
}
// Register hook for the standard KeyValueStore getter
PLT_HOOK_REGISTER(dev, inode, _ZNK3art9OatHeader20GetKeyValueStoreSizeEv);
PLT_HOOK_REGISTER(dev, inode, _ZNK3art9OatHeader16GetKeyValueStoreEv);
// If the standard store hook fails (e.g., on Android 16+), try the Checksum hook
if (!lsplt::CommitHook()) {
PLT_HOOK_REGISTER(dev, inode, _ZNK3art9OatHeader15ComputeChecksumEPj);
lsplt::CommitHook();

View File

@ -1,5 +1,4 @@
allow dex2oat dex2oat_exec file execute_no_trans
allow dex2oat system_linker_exec file execute_no_trans
allow shell shell dir write

View File

@ -12,6 +12,8 @@ import org.lsposed.lspd.service.BridgeService;
import org.lsposed.lspd.util.Utils;
import io.github.libxposed.api.XposedInterface;
import io.github.libxposed.api.annotations.AfterInvocation;
import io.github.libxposed.api.annotations.XposedHooker;
public class ParasiticManagerSystemHooker implements HandleSystemServerProcessHooker.Callback {
@ -31,8 +33,10 @@ public class ParasiticManagerSystemHooker implements HandleSystemServerProcessHo
}
}*/
@XposedHooker
private static class Hooker implements XposedInterface.Hooker {
public static void after(XposedInterface.AfterHookCallback callback) throws Throwable {
@AfterInvocation
public static void afterHookedMethod(XposedInterface.AfterHookCallback callback) throws Throwable {
var intent = (Intent) callback.getArgs()[0];
if (intent == null) return;
if (!intent.hasCategory("org.lsposed.manager.LAUNCH_MANAGER")) return;

View File

@ -1,29 +1,33 @@
# LSPosed v1.11.0 🎐
🎉 To celebrate the release of Android 16, we are excited to announce a new stable version of LSPosed!
This release brings major improvements for **Android 16 Beta** readiness, resolves specific quirks on Android 10 and OnePlus devices, and significantly reinforces overall system stability.
To better understand LSPosed, we recommend reading our [troubleshooting guide](https://github.com/JingMatrix/LSPosed/issues/123).
### 📱 Compatibility & Core
* **Android 16 Beta Support:** Fixed compatibility issues with Android 16 QPR Beta 3 (specifically `UserManager` changes) and recent ART updates affecting the `dex2oat` wrapper.
* **Android 10 Fixes:** Resolved `dex2oat` crashes caused by 32-bit/64-bit architecture mismatches.
* **OnePlus Compatibility:** Restored `Application#attach` hooking capabilities, overcoming aggressive method inlining found in recent OOS updates.
* **Dex2Oat Overhaul:** Refactored the wrapper to utilize the APEX linker directly, eliminating missing symbol errors and boosting reliability.
### ✨ What's New
* Fully support Android 16.
* Hide traces introduced by the `dex2oat` hook.
* The LSPosed manager can now be opened via the Action button.
* New options have been added to the `Select` menu for scopes.
* Allow users to toggle off detectable logging of LSPosed.
### 🛠️ Stability & Fixes
* **Database Integrity:** Resolved critical crashes and potential corruption during database initialization and migration.
* **Frida Compatibility:** Fixed `SIGSEGV` crashes when running alongside Frida by making memory mapping parsing more robust.
* **SELinux:** Corrected file contexts for the modern Xposed API 100 (`openRemoteFile`) and ensured they persist across reboots.
* **Injection Reliability:** Implemented retry logic for System Server injection to minimize start-up failures.
### 🐛 Bug Fixes
* The `LSPlt` hook has been abandoned for efficiency considerations.
* Resolved an issue where modules targeting `systemui` (e.g., `ClassicPowerMenu`) were not working.
* Removed Telemetry monitoring.
### ⚡ Internal Changes
* **Kotlin Refactor:** The `DexParser` has been rewritten in Kotlin for improved performance and maintainability.
* **WebUI Removal:** Removed the WebUI integration as it is no longer required.
### 🔄 Other Changes
* The dependency on `topjohnwu/libcxx` has been removed in favor of the official C++ implementation. This will result in a larger release archive for LSPosed.
---
### 🚀 High-Priority Plans
* Creating comprehensive development documentation for LSPosed.
* Resolving open issues with assignees.
## 🔮 Development Plan
**Full Changelog**: [v1.10.1...v1.10.2](https://github.com/JingMatrix/LSPosed/compare/v1.10.1...v1.10.2)
The current LSPosed fork is undergoing a complete refactor into a new project: **Vector**.
<details>
<summary>❤️ A personal note</summary>
We are in the process of rewriting the Java layer into Kotlin and adding extensive documentation for the native layer.
For the past few months, I have been focused on finishing my PhD thesis manuscript, which has limited my active development on LSPosed. I sincerely appreciate the community's passion and support during this time. It has been a pleasure to witness our community grow and thrive around this open-source fork. I am deeply indebted to your trust and respect, which has indeed helped me navigate the unavoidable challenges and depressions faced by a PhD candidate.
The name **Vector** was chosen to manifest its close mathematical relationship with **Matrix**, while symbolizing the framework's role as a precise injection vector for modules.
Maintaining this project is a joyful responsibility. However, life is a grand museum of passions, and I am constantly called by my devotion to research and teaching at the university. For users eagerly awaiting new features, I want to reassure you that the LSPosed codebase is quite stable and sufficient for its functionality. Moreover, I sincerely encourage developers to join the project. For all users, please consider participating in the [GitHub Discussions](https://github.com/JingMatrix/LSPosed/discussions) to share your experiences and various tips. Nothing is more valuable to an open-source project than an active community.
</details>

View File

@ -1,6 +1,6 @@
{
"version": "v1.11.0",
"versionCode": 7209,
"zipUrl": "https://github.com/JingMatrix/LSPosed/releases/download/v1.11.0/LSPosed-v1.11.0-7209-zygisk-release.zip",
"version": "v1.10.2",
"versionCode": 7182,
"zipUrl": "https://github.com/JingMatrix/LSPosed/releases/download/v1.10.2/LSPosed-v1.10.2-7182-zygisk-release.zip",
"changelog": "https://raw.githubusercontent.com/JingMatrix/LSPosed/master/magisk-loader/update/changelog.md"
}

@ -1 +1 @@
Subproject commit ec7cb26aa215b20b2ab1baf7c7b5a91a021a9016
Subproject commit 496b76fa3e5af87958ebef97bd160319e05da79b

@ -1 +1 @@
Subproject commit 745afd390c7b87d9cd31efd246a7bc12cc190ac0
Subproject commit 54582730315ba4a3d7cfaf9baf9d23c419e07006