diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index a094e04..69004a0 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -91,7 +91,7 @@ jobs: - api-level: 36 target: aosp_atd arch: x86_64 - - api-level: 36.0-CANARY + - api-level: CANARY target: google_apis_ps16k arch: x86_64 steps: @@ -116,8 +116,17 @@ jobs: arch: ${{ matrix.arch }} target: ${{ matrix.target }} channel: canary - script: ./gradlew connectedCheck + script: | + ./gradlew connectedCheck -Pandroid.injected.androidTest.leaveApksInstalledAfterRun=true + ./gradlew connectedCheck -Pandroid.testInstrumentationRunnerArguments.load=true force-avd-creation: false emulator-options: -no-snapshot -read-only -no-window -gpu swiftshader_indirect -no-audio -no-boot-anim -camera-back none disable-animations: true cores: 4 + - name: Upload outputs + if: ${{ failure() }} + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.api-level }}_${{ matrix.arch }}_outputs + path: library/build/outputs + compression-level: 9 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index edcd044..b7e1c04 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,10 +1,10 @@ [versions] [libraries] -androidx-annotation = { module = "androidx.annotation:annotation", version= "1.9.1" } -test-ext-junit = { module = "androidx.test.ext:junit", version = "1.2.1" } -test-rules = { module = "androidx.test:rules", version = "1.6.1" } +androidx-annotation = { module = "androidx.annotation:annotation", version = "1.9.1" } +test-ext-junit = { module = "androidx.test.ext:junit", version = "1.3.0" } +test-rules = { module = "androidx.test:rules", version = "1.7.0" } [plugins] -agp-lib = { id = "com.android.library", version = "8.8.0" } +agp-lib = { id = "com.android.library", version = "8.13.0" } lsplugin-jgit = { id = "org.lsposed.lsplugin.jgit", version = "1.1" } lsplugin-publish = { id = "org.lsposed.lsplugin.publish", version = "1.1" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index a4b76b9..8bdaf60 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index cea7a79..2a84e18 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index f3b75f3..ef07e01 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright © 2015-2021 the original authors. +# Copyright © 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -114,7 +114,7 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar +CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. @@ -205,7 +205,7 @@ fi DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. @@ -213,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index 9d21a21..db3a6ac 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -70,11 +70,11 @@ goto fail :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar +set CLASSPATH= @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell diff --git a/library/build.gradle.kts b/library/build.gradle.kts index 58c7a14..aba6b88 100644 --- a/library/build.gradle.kts +++ b/library/build.gradle.kts @@ -17,20 +17,22 @@ plugins { } android { - compileSdk = 35 - buildToolsVersion = "35.0.1" + compileSdk = 36 + buildToolsVersion = "36.1.0" namespace = "org.lsposed.hiddenapibypass.library" buildFeatures { - androidResources = false buildConfig = true } + androidResources { + enable = false + } defaultConfig { minSdk = 1 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } testOptions { - targetSdk = 35 + targetSdk = 36 } buildTypes { release { @@ -63,12 +65,6 @@ dependencies { androidTestCompileOnly(projects.stub) } -androidComponents.onVariants { variant -> - variant.instrumentation.transformClassesWith( - ClassVisitorFactory::class.java, InstrumentationScope.PROJECT - ) {} -} - abstract class ClassVisitorFactory : AsmClassVisitorFactory { override fun createClassVisitor( classContext: ClassContext, @@ -102,7 +98,7 @@ abstract class ManifestUpdater : DefaultTask() { fun taskAction() { outputManifest.get().asFile.writeText( mergedManifest.get().asFile.readText() - .replace(" ManifestUpdater::outputManifest ) .toTransform(SingleArtifact.MERGED_MANIFEST) + variant.instrumentation.transformClassesWith( + ClassVisitorFactory::class.java, InstrumentationScope.PROJECT + ) {} } diff --git a/library/src/androidTest/java/org/lsposed/hiddenapibypass/HiddenApiBypassTest.java b/library/src/androidTest/java/org/lsposed/hiddenapibypass/HiddenApiBypassTest.java index c108c43..699492f 100644 --- a/library/src/androidTest/java/org/lsposed/hiddenapibypass/HiddenApiBypassTest.java +++ b/library/src/androidTest/java/org/lsposed/hiddenapibypass/HiddenApiBypassTest.java @@ -1,6 +1,8 @@ package org.lsposed.hiddenapibypass; import static org.hamcrest.core.StringContains.containsString; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertSame; @@ -9,10 +11,13 @@ import android.content.pm.ApplicationInfo; import android.graphics.drawable.ClipDrawable; import android.os.Build; +import android.os.Process; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SdkSuppress; +import androidx.test.platform.app.InstrumentationRegistry; +import org.junit.BeforeClass; import org.junit.FixMethodOrder; import org.junit.Rule; import org.junit.Test; @@ -39,6 +44,20 @@ public class HiddenApiBypassTest { public HiddenApiBypassTest() throws ClassNotFoundException { } + @BeforeClass + public static void setUp() { + var context = InstrumentationRegistry.getInstrumentation().getContext(); + Helper.enableOffsetCache(context); + } + + @Test + public void AAtestCachedDataLoaded() { + var loaded = Helper.getCachedOffsetData() != null; + var arguments = InstrumentationRegistry.getArguments(); + var load = arguments.containsKey("load"); + assertEquals(loaded, load); + } + @Test public void AgetDeclaredMethods() { List methods = HiddenApiBypass.getDeclaredMethods(runtime); @@ -142,4 +161,25 @@ class X { assertTrue(Helper.checkArgsForInvokeMethod(new Class[]{Object.class, int.class, byte.class, short.class, char.class, double.class, float.class, boolean.class, long.class}, new Object[]{new X(), 1, (byte) 0, (short) 2, 'c', 1.1, 1.2f, false, 114514L})); } + @Test + public void PtestCachedOffset() { + var context = InstrumentationRegistry.getInstrumentation().getContext(); + var artVersion = Helper.getArtVersion(context); + var isOld = artVersion == -1L; + var isNew = artVersion >= 36_00_00000L; + var is64bit = Process.is64Bit(); + var data = new long[10]; + data[0] = 24; + data[1] = 12; + data[2] = 24; + data[3] = 48; + data[4] = 40; + data[5] = isNew ? 40 : 56; + data[6] = isOld ? is64bit ? 40 : 28 : is64bit ? 32 : 24; + data[7] = is64bit ? 8 : 4; + data[8] = 16; + data[9] = 4; + assertArrayEquals("art version " + artVersion, data, Helper.getCachedOffsetData()); + } + } diff --git a/library/src/main/java/org/lsposed/hiddenapibypass/Helper.java b/library/src/main/java/org/lsposed/hiddenapibypass/Helper.java index 1fb7fd2..c9ad934 100644 --- a/library/src/main/java/org/lsposed/hiddenapibypass/Helper.java +++ b/library/src/main/java/org/lsposed/hiddenapibypass/Helper.java @@ -16,14 +16,92 @@ package org.lsposed.hiddenapibypass; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Build; + +import androidx.annotation.RequiresApi; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; import java.lang.invoke.MethodType; import java.util.HashSet; import java.util.Set; +@RequiresApi(Build.VERSION_CODES.P) @SuppressWarnings("unused") public class Helper { static final Set signaturePrefixes = new HashSet<>(); + private static long[] cachedOffsetData = null; + + private static File cacheFile = null; + private static long artVersion = 0L; + + public static long[] getCachedOffsetData() { + return cachedOffsetData; + } + + public static void setCachedOffsetData(long[] data) { + if (cachedOffsetData != null || data.length != 10) return; + cachedOffsetData = data; + + if (cacheFile == null) return; + try (var fos = new FileOutputStream(cacheFile); + var oos = new ObjectOutputStream(fos)) { + oos.writeUTF(Build.FINGERPRINT); + oos.writeLong(artVersion); + oos.writeObject(cachedOffsetData); + } catch (IOException ignored) { + } + } + + public static void enableOffsetCache(Context context) { + cacheFile = new File(context.getCacheDir(), "HiddenApiBypass"); + artVersion = getArtVersion(context); + + try (var fis = new FileInputStream(cacheFile); + var ois = new ObjectInputStream(fis)) { + var fingerprint = ois.readUTF(); + if (!Build.FINGERPRINT.equals(fingerprint)) return; + var art = ois.readLong(); + if (artVersion != art) return; + cachedOffsetData = (long[]) ois.readObject(); + } catch (Exception ignored) { + } + } + + public static long getArtVersion(Context context) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return -1L; + var pm = context.getPackageManager(); + try { + var moduleInfo = pm.getModuleInfo("com.android.art", 1); + var name = moduleInfo.getPackageName(); + if (name == null) return -2L; + var info = pm.getPackageInfo(name, PackageManager.MATCH_APEX); + return info.getLongVersionCode(); + } catch (PackageManager.NameNotFoundException e) { + try (var file = new FileReader("/proc/self/mountinfo"); + var reader = new BufferedReader(file)) { + var line = reader.lines() + .filter(s -> s.contains(" / /apex/com.android.art@")) + .findAny(); + if (!line.isPresent()) return -3L; + var part = line.get().split("@", 2)[1]; + var versionStr = part.split(" ", 2)[0]; + return Long.parseLong(versionStr); + } catch (Exception e2) { + return -4L; + } + } + } + static boolean checkArgsForInvokeMethod(java.lang.Class[] params, Object[] args) { if (params.length != args.length) return false; for (int i = 0; i < params.length; ++i) { diff --git a/library/src/main/java/org/lsposed/hiddenapibypass/HiddenApiBypass.java b/library/src/main/java/org/lsposed/hiddenapibypass/HiddenApiBypass.java index f440e34..c131960 100644 --- a/library/src/main/java/org/lsposed/hiddenapibypass/HiddenApiBypass.java +++ b/library/src/main/java/org/lsposed/hiddenapibypass/HiddenApiBypass.java @@ -61,60 +61,94 @@ public final class HiddenApiBypass { try { //noinspection JavaReflectionMemberAccess DiscouragedPrivateApi unsafe = (Unsafe) Unsafe.class.getDeclaredMethod("getUnsafe").invoke(null); - assert unsafe != null; - ClassLoader bootClassloader = new CoreOjClassLoader(); - Class executableClass = bootClassloader.loadClass(Executable.class.getName()); - Class methodHandleClass = bootClassloader.loadClass(MethodHandle.class.getName()); - Class classClass = bootClassloader.loadClass(Class.class.getName()); - methodOffset = unsafe.objectFieldOffset(executableClass.getDeclaredField("artMethod")); - classOffset = unsafe.objectFieldOffset(executableClass.getDeclaredField("declaringClass")); - artOffset = unsafe.objectFieldOffset(methodHandleClass.getDeclaredField("artFieldOrMethod")); - long iField; - long sField; - try { - iField = unsafe.objectFieldOffset(classClass.getDeclaredField("fields")); - sField = iField; - } catch (NoSuchFieldException e) { - iField = unsafe.objectFieldOffset(classClass.getDeclaredField("iFields")); - sField = unsafe.objectFieldOffset(classClass.getDeclaredField("sFields")); + var data = Helper.getCachedOffsetData(); + if (data == null) { + data = readOffsetData(); + Helper.setCachedOffsetData(data); + } else if (BuildConfig.DEBUG) { + Log.d(TAG, "Using cached offset data"); } - iFieldOffset = iField; - sFieldOffset = sField; - methodsOffset = unsafe.objectFieldOffset(classClass.getDeclaredField("methods")); - Method mA = Helper.NeverCall.class.getDeclaredMethod("a"); - Method mB = Helper.NeverCall.class.getDeclaredMethod("b"); - mA.setAccessible(true); - mB.setAccessible(true); - MethodHandle mhA = MethodHandles.lookup().unreflect(mA); - MethodHandle mhB = MethodHandles.lookup().unreflect(mB); - long aAddr = unsafe.getLong(mhA, artOffset); - long bAddr = unsafe.getLong(mhB, artOffset); - long aMethods = unsafe.getLong(Helper.NeverCall.class, methodsOffset); - artMethodSize = bAddr - aAddr; - if (BuildConfig.DEBUG) Log.v(TAG, artMethodSize + " " + - Long.toString(aAddr, 16) + ", " + - Long.toString(bAddr, 16) + ", " + - Long.toString(aMethods, 16)); - artMethodBias = aAddr - aMethods - artMethodSize; - Field fI = Helper.NeverCall.class.getDeclaredField("i"); - Field fJ = Helper.NeverCall.class.getDeclaredField("j"); - fI.setAccessible(true); - fJ.setAccessible(true); - MethodHandle mhI = MethodHandles.lookup().unreflectGetter(fI); - MethodHandle mhJ = MethodHandles.lookup().unreflectGetter(fJ); - long iAddr = unsafe.getLong(mhI, artOffset); - long jAddr = unsafe.getLong(mhJ, artOffset); - long iFields = unsafe.getLong(Helper.NeverCall.class, iFieldOffset); - artFieldSize = jAddr - iAddr; - if (BuildConfig.DEBUG) Log.v(TAG, artFieldSize + " " + - Long.toString(iAddr, 16) + ", " + - Long.toString(jAddr, 16) + ", " + - Long.toString(iFields, 16)); - artFieldBias = iAddr - iFields; + methodOffset = data[0]; + classOffset = data[1]; + artOffset = data[2]; + methodsOffset = data[3]; + iFieldOffset = data[4]; + sFieldOffset = data[5]; + artMethodSize = data[6]; + artMethodBias = data[7]; + artFieldSize = data[8]; + artFieldBias = data[9]; } catch (ReflectiveOperationException e) { Log.e(TAG, "Initialize error", e); throw new ExceptionInInitializerError(e); } + + } + + private static long[] readOffsetData() throws ReflectiveOperationException { + ClassLoader bootClassloader = new CoreOjClassLoader(); + Class executableClass = bootClassloader.loadClass(Executable.class.getName()); + Class methodHandleClass = bootClassloader.loadClass(MethodHandle.class.getName()); + Class classClass = bootClassloader.loadClass(Class.class.getName()); + var methodOffset = unsafe.objectFieldOffset(executableClass.getDeclaredField("artMethod")); + var classOffset = unsafe.objectFieldOffset(executableClass.getDeclaredField("declaringClass")); + var artOffset = unsafe.objectFieldOffset(methodHandleClass.getDeclaredField("artFieldOrMethod")); + var methodsOffset = unsafe.objectFieldOffset(classClass.getDeclaredField("methods")); + + long iField; + long sField; + try { + iField = unsafe.objectFieldOffset(classClass.getDeclaredField("fields")); + sField = iField; + } catch (NoSuchFieldException e) { + iField = unsafe.objectFieldOffset(classClass.getDeclaredField("iFields")); + sField = unsafe.objectFieldOffset(classClass.getDeclaredField("sFields")); + } + + Method mA = Helper.NeverCall.class.getDeclaredMethod("a"); + Method mB = Helper.NeverCall.class.getDeclaredMethod("b"); + mA.setAccessible(true); + mB.setAccessible(true); + MethodHandle mhA = MethodHandles.lookup().unreflect(mA); + MethodHandle mhB = MethodHandles.lookup().unreflect(mB); + long aAddr = unsafe.getLong(mhA, artOffset); + long bAddr = unsafe.getLong(mhB, artOffset); + long aMethods = unsafe.getLong(Helper.NeverCall.class, methodsOffset); + var artMethodSize = bAddr - aAddr; + if (BuildConfig.DEBUG) Log.v(TAG, artMethodSize + " " + + Long.toString(aAddr, 16) + ", " + + Long.toString(bAddr, 16) + ", " + + Long.toString(aMethods, 16)); + var artMethodBias = aAddr - aMethods - artMethodSize; + + Field fI = Helper.NeverCall.class.getDeclaredField("i"); + Field fJ = Helper.NeverCall.class.getDeclaredField("j"); + fI.setAccessible(true); + fJ.setAccessible(true); + MethodHandle mhI = MethodHandles.lookup().unreflectGetter(fI); + MethodHandle mhJ = MethodHandles.lookup().unreflectGetter(fJ); + long iAddr = unsafe.getLong(mhI, artOffset); + long jAddr = unsafe.getLong(mhJ, artOffset); + long iFields = unsafe.getLong(Helper.NeverCall.class, iField); + var artFieldSize = jAddr - iAddr; + if (BuildConfig.DEBUG) Log.v(TAG, artFieldSize + " " + + Long.toString(iAddr, 16) + ", " + + Long.toString(jAddr, 16) + ", " + + Long.toString(iFields, 16)); + var artFieldBias = iAddr - iFields; + + long[] data = new long[10]; + data[0] = methodOffset; + data[1] = classOffset; + data[2] = artOffset; + data[3] = methodsOffset; + data[4] = iField; + data[5] = sField; + data[6] = artMethodSize; + data[7] = artMethodBias; + data[8] = artFieldSize; + data[9] = artFieldBias; + return data; } /** diff --git a/settings.gradle.kts b/settings.gradle.kts index 4201c79..59be106 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,17 +1,32 @@ +@file:Suppress("UnstableApiUsage") + enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") +enableFeaturePreview("STABLE_CONFIGURATION_CACHE") pluginManagement { repositories { - gradlePluginPortal() - google() + google { + content { + includeGroupAndSubgroups("androidx") + includeGroupAndSubgroups("com.android") + includeGroupAndSubgroups("com.google") + } + } mavenCentral() + gradlePluginPortal() } } dependencyResolutionManagement { repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS repositories { - google() + google { + content { + includeGroupAndSubgroups("androidx") + includeGroupAndSubgroups("com.android") + includeGroupAndSubgroups("com.google") + } + } mavenCentral() } }