diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..baee9c7 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,13 @@ +name: ci +on: [push, pull_request] +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-java@v1 + with: + java-version: 1.8 + - uses: gradle/wrapper-validation-action@v1 + - run: ./gradlew :librootjava:build + diff --git a/build.gradle b/build.gradle index 9a78710..d168446 100644 --- a/build.gradle +++ b/build.gradle @@ -1,19 +1,19 @@ buildscript { repositories { google() - jcenter() + mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:3.2.1' + classpath 'com.android.tools.build:gradle:4.1.3' classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1' - classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.4' } } allprojects { repositories { google() - jcenter() + maven { url 'https://jitpack.io' } + mavenCentral() mavenLocal() } } diff --git a/gradle.properties b/gradle.properties index 1d3591c..9c36dce 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,18 +1,15 @@ -# Project-wide Gradle settings. - -# IDE (e.g. Android Studio) users: -# Gradle settings configured through the IDE *will override* -# any settings specified in this file. - -# For more details on how to configure your build environment visit +## For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html - +# # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. -# Default value: -Xmx10248m -XX:MaxPermSize=256m +# Default value: -Xmx1024m -XX:MaxPermSize=256m # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 - +# # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects -# org.gradle.parallel=true \ No newline at end of file +# org.gradle.parallel=true +#Sat Feb 05 12:08:52 CET 2022 +#android.enableJetifier=true +android.useAndroidX=true diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 48effd2..44d10f7 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Fri Nov 09 13:15:43 CET 2018 +#Sat Feb 05 12:08:34 CET 2022 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/jitpack.yml b/jitpack.yml new file mode 100644 index 0000000..d1e6c1e --- /dev/null +++ b/jitpack.yml @@ -0,0 +1,2 @@ +install: + - ./gradlew clean :librootjava:install :librootjavadaemon:install diff --git a/librootjava/README.md b/librootjava/README.md index 94d4558..b866116 100644 --- a/librootjava/README.md +++ b/librootjava/README.md @@ -1,11 +1,14 @@ # libRootJava +[![ci][1]][2] [![](https://jitpack.io/v/eu.chainfire/librootjava.svg)](https://jitpack.io/#eu.chainfire/librootjava) + Run Java (and Kotlin) code as root! - Runs code directly from your APK - Access to all the classes in your projects - Access to Android classes - Easy Binder-based IPC/RPC +- Debugging support ## License @@ -19,6 +22,18 @@ crediting me is appreciated. If you modify the library itself when you use it in your projects, you are kindly requested to share the sources of those modifications. +## Deprecated + +This library is not under active development right now, as I've mostly +moved away from the Android world. While I believe it still works great, +if it breaks due to changes on new Android versions or root solutions, +fixes may be slow to appear. + +If you're writing a new app, you might consider using +[TopJohnWu's libsu](https://github.com/topjohnwu/libsu) instead. Barring +some edge-cases (that I personally seem to be the biggest user of) the +capabilities should be similar, but it's likely to be better maintained. + ## Spaghetti Sauce Project This library is part of the [Spaghetti Sauce Project](https://github.com/Chainfire/spaghetti_sauce_project). @@ -69,14 +84,6 @@ While this library was originally built to support Android 4.2+ devices, it only officially supports 5.0+. The first public GitHub release was tested specifically on 5.0, 7.0, 8.0, and 9.0. -## Debugging - -Debugging the code running as root is currently not supported. I made -some headway getting the jdwp server running, but I've not been able -to successfully connect jdb or AndroidStudio to it. If you want to take -a stab at it, there are some comments in -```RootJava.getLaunchString()``` related to it. - ## Recommended reading I strongly recommend you read the library's source code in its entirety @@ -270,6 +277,96 @@ non-root code. (See the [example project](../librootjava_example) for a more elaborate example for this entire process) +#### Cleanup + +To execute our code as root, files may need to be created in our app's +cache directory. While the library does its best to clean up after +itself, there are situations (such as a reboot mid-process) where some +files may not automatically be removed. + +It is recommended to periodically call ```RootJava.cleanupCache()``` +to remove these leftover files. This method will cleanup all of our +files in the cache directory that predate the current boot. As I/O is +performed, this method should not be called from the main UI thread. + +#### Debugging + +Debugging is supported since version 1.1.0, but disabled by default. + +To enable debugging, first we must tell the non-root process to launch +our root process with debug support enabled. We do this by calling +```Debugger.setEnabled()``` *before* calling +```RootJava.getLaunchScript()```: + +``` +public class MyActivity { + // ... + private void launchRootProcess() { + // ... + + Debugger.setEnabled(BuildConfig.DEBUG); + rootShell.addCommand(RootJava.getLaunchScript(this, RootMain.class, null, null, null, BuildConfig.APPLICATION_ID + ":root")); + } +} +``` + +We use ```BuildConfig.DEBUG``` instead of ```true``` to prevent +potential issues with release builds. + +In the code running as root, we then call ```Debugger.waitFor()``` +to pause execution (of the current thread) until a debugger is connected: + +``` +public class RootMain { + public static void main(String[] args) { + RootJava.restoreOriginalLdLibraryPath(); // call this first! + + if (BuildConfig.DEBUG) { + Debugger.waitFor(true); // wait for connection + } + + setYourBreakpointHere(); + } +} +``` + +We wrap the call inside a ```BuildConfig.DEBUG``` check, again to prevent +issues with release builds. + +Note that for long-running processes (such as daemons) you may not want +to explicitly wait for a debugger connection, in that case you can use +the ```Debugger.setName()``` method instead. That method may also be +called *before* ```Debugger.waitFor()``` to customize the debugger +display name, as by default the process name is used. + +Now that debugging is enabled, we still need to actually connect to +the process. You can do this in Android Studio via the *Attach +debugger to Android process* option in the *Run* menu. Once the root +process is running, it will be listed in the popup window. + +Note that you can debug *both* the non-root *and* root process at the +same time. While Android Studio can only debug a single application +*launched* in debug mode, you can *attach* to multiple processes. So +you can simply run your application in debug mode, have it launch the +root process, then *attach* to that root process, and debug both +simultaneously. + +Android Studio of course has no knowledge of the relation between the +multiple processes being debugged, so stepping into an IPC call in one +process will not automatically break on the implementation code in the +other process. You will have to set those breakpoints manually. + +#### BuildConfig + +The example snippets and projects make extensive use of the BuildConfig +class. This class is generated during the build process for every +module, library, etc. Double check you are importing the correct +BuildConfig class, the one from your application's package (unless you +know exactly what you're doing). Note that this complicates putting this +code inside libraries and modules, as you cannot reference the +application's BuildConfig class from a library. Work-arounds are beyond +the scope of this README. + #### Daemonizing For some use-cases you may want to run your root process as a daemon, @@ -308,14 +405,69 @@ If for some reason Gradle does not pick up these rules automatically, copy/paste them from the ```proguard.txt``` file into your own ProGuard ruleset. +## Restrictions on non-SDK interfaces + +Android 9.0 (Pie, API 28) introduces restrictions on the use of non-SDK +interfaces, whether directly, via reflection, or via JNI. See [this page +on the Android site](https://developer.android.com/about/versions/pie/restrictions-non-sdk-interfaces) +for details. + +We do use non-SDK interfaces in the part of the code that runs as root, +and currently it seems this is exempt from the restrictions. My +preliminary interpretation of the relevant sources in AOSP is that +this only applies to code running in a process spawned from Zygote, +which our code running as root isn't. + +It appears to currently (November 2018) be implemented as runtime +checks only. It may create some problems if these checks in the future +are also done at the ahead-of-time (AOT) compilation stage at app +install. There is currently no indication if that is or isn't likely to +happen (but if it does I expect the runtime checks to remain as well). + +In my experiments so far, if API 28 is targeted and you use a non-SDK +call in a normal app, a warning is written to the logs. If you go one +step further and enable strict-mode, a full stack-trace is logged: + +``` +StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() + .detectNonSdkApiUsage() + .build()); +} +``` + +However, making a non-SDK call from the code running as root doesn't +seem to trigger anything, either with or without explicitly enabling +strict mode. So it appears for now we are safe. + +TODO: This needs further investigation and monitoring, especially when +an Android 10 preview comes out! + ## Gradle +Root `build.gradle`: + ``` -implementation 'eu.chainfire:librootjava:1.0.0' +allprojects { + repositories { + ... + maven { url 'https://jitpack.io' } + } +} +``` + +Module `build.gradle`: + +``` +dependencies { + implementation 'eu.chainfire:librootjava:1.3.3' +} ``` ## Notes This library includes its own Logger class that is used throughout, which should probably have been refactored out. -It wasn't. \ No newline at end of file +It wasn't. + +[1]: https://github.com/Chainfire/librootjava/workflows/ci/badge.svg +[2]: https://github.com/Chainfire/librootjava/actions diff --git a/librootjava/build.gradle b/librootjava/build.gradle index d2d8a82..5ff11ec 100644 --- a/librootjava/build.gradle +++ b/librootjava/build.gradle @@ -1,10 +1,9 @@ apply plugin: 'com.android.library' apply plugin: 'com.github.dcendents.android-maven' -apply plugin: 'com.jfrog.bintray' android { - compileSdkVersion 26 - buildToolsVersion '28.0.3' + compileSdkVersion 30 + buildToolsVersion '30.0.3' defaultConfig { minSdkVersion 21 /* was 14 pre-Binder/AIDL */ targetSdkVersion 26 @@ -51,7 +50,7 @@ ext { gitUrl = 'https://github.com/Chainfire/librootjava.git' issueTrackerUrl = 'https://github.com/Chainfire/librootjava/issues' - libraryVersion = '1.0.0' + libraryVersion = '1.3.3' developerId = 'Chainfire' developerName = 'Jorrit Jongma' @@ -60,9 +59,6 @@ ext { licenseName = 'The Apache Software License, Version 2.0' licenseUrl = 'http://www.apache.org/licenses/LICENSE-2.0.txt' allLicenses = ["Apache-2.0"] - - bintrayRepo = 'maven' - bintrayName = artifact } task installMavenLocal(type: Upload) { @@ -72,42 +68,14 @@ task installMavenLocal(type: Upload) { packaging 'aar' groupId = publishedGroupId artifactId = artifact - version = '1.0.0-SNAPSHOT' + version = libraryVersion + '-SNAPSHOT' } } } -// Workaround bintray bug ignoring these from pom and bintray settings version = libraryVersion group = publishedGroupId -bintray { - Properties properties = new Properties() - properties.load(project.rootProject.file('local.properties').newDataInputStream()) - user = properties.getProperty('bintray.user') - key = properties.getProperty('bintray.apikey') - - configurations = ['archives'] - dryRun = false - publish = true - pkg { - repo = bintrayRepo - name = libraryName - desc = libraryDescription - websiteUrl = siteUrl - issueTrackerUrl = issueTrackerUrl // doesn't actually work? - vcsUrl = gitUrl - //githubRepo = gitUrl // some more bintray weirdness here, breaks upload - //githubReleaseNotesFile = 'README.md' - licenses = allLicenses - publicDownloadNumbers = true - version { - name = libraryVersion - released = new Date() - } - } -} - install { repositories.mavenInstaller { pom.project { @@ -138,5 +106,3 @@ install { } } } - -bintrayUpload.dependsOn install diff --git a/librootjava/src/main/java/eu/chainfire/librootjava/AppProcess.java b/librootjava/src/main/java/eu/chainfire/librootjava/AppProcess.java index 6062b8f..4698b13 100644 --- a/librootjava/src/main/java/eu/chainfire/librootjava/AppProcess.java +++ b/librootjava/src/main/java/eu/chainfire/librootjava/AppProcess.java @@ -27,7 +27,6 @@ import java.io.IOException; import java.util.List; import java.util.Locale; -import java.util.UUID; /** * Utility methods to determine the location and bits of the app_process executable to be used.
@@ -43,12 +42,12 @@ public class AppProcess { /** * Toolbox or toybox? */ - public static final String box = Build.VERSION.SDK_INT < 23 ? "toolbox" : "toybox"; + public static final String BOX = Build.VERSION.SDK_INT < 23 ? "toolbox" : "toybox"; /** * Used to create unique filenames in common locations */ - public static final String uuid = getUUID(); + public static final String UUID = getUUID(); /** * @return uuid that doesn't contain 32 or 64, as to not confuse bit-choosing code @@ -56,7 +55,7 @@ public class AppProcess { private static String getUUID() { String uuid = null; while ((uuid == null) || uuid.contains("32") || uuid.contains("64")) { - uuid = UUID.randomUUID().toString(); + uuid = java.util.UUID.randomUUID().toString(); } return uuid; } @@ -284,6 +283,29 @@ public static boolean guessIfAppProcessIs64Bits(String app_process) { return !isRunningAs32BitOn64BitArch(); } + /** + * Should app_process be relocated ?
+ *
+ * On older Android versions we must relocate the app_process binary to prevent it from + * running in a restricted SELinux context. On Q this presents us with the linker error: + * "Error finding namespace of apex: no namespace called runtime". However, at least + * on the first preview release of Q, running straight from /system/bin works and does + * not give us a restricted SELinux context, so we skip relocation. + * + * TODO: Revisit on new Q preview and production releases. Maybe spend some time figuring out what causes the namespace error and if we can fix it. + * + * @see #getAppProcessRelocate(Context, String, List, List, String) + * + * @return should app_process be relocated ? + */ + @TargetApi(Build.VERSION_CODES.M) + public static boolean shouldAppProcessBeRelocated() { + return !( + (Build.VERSION.SDK_INT >= 29) || + ((Build.VERSION.SDK_INT == 28) && (Build.VERSION.PREVIEW_SDK_INT != 0)) + ); + } + /** * Create script to relocate specified app_process binary to a different location.
*
@@ -291,6 +313,7 @@ public static boolean guessIfAppProcessIs64Bits(String app_process) { * SELinux context that we do not want. Relocating it bypasses that.
* * @see #getAppProcess() + * @see #shouldAppProcessBeRelocated() * * @param context Application or activity context * @param appProcessBase Path to original app_process or null for default @@ -302,6 +325,10 @@ public static boolean guessIfAppProcessIs64Bits(String app_process) { public static String getAppProcessRelocate(Context context, String appProcessBase, List preLaunch, List postExecution, String path) { if (appProcessBase == null) appProcessBase = getAppProcess(); if (path == null) { + if (!shouldAppProcessBeRelocated()) { + return appProcessBase; + } + path = "/dev"; if ((context.getApplicationInfo().flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE) == 0) { File cacheDir = context.getCacheDir(); @@ -321,15 +348,18 @@ public static String getAppProcessRelocate(Context context, String appProcessBas } } + boolean onData = path.startsWith("/data/"); + String appProcessCopy; if (guessIfAppProcessIs64Bits(appProcessBase)) { - appProcessCopy = path + "/.app_process64_" + uuid; + appProcessCopy = path + "/.app_process64_" + UUID; } else { - appProcessCopy = path + "/.app_process32_" + uuid; + appProcessCopy = path + "/.app_process32_" + UUID; } - preLaunch.add(String.format(Locale.ENGLISH, "%s cp %s %s >/dev/null 2>/dev/null", box, appProcessBase, appProcessCopy)); - preLaunch.add(String.format(Locale.ENGLISH, "%s chmod 0700 %s >/dev/null 2>/dev/null", box, appProcessCopy)); - postExecution.add(String.format(Locale.ENGLISH, "%s rm %s >/dev/null 2>/dev/null", box, appProcessCopy)); + preLaunch.add(String.format(Locale.ENGLISH, "%s cp %s %s >/dev/null 2>/dev/null", BOX, appProcessBase, appProcessCopy)); + preLaunch.add(String.format(Locale.ENGLISH, "%s chmod %s %s >/dev/null 2>/dev/null", BOX, onData ? "0766" : "0700", appProcessCopy)); + if (onData) preLaunch.add(String.format(Locale.ENGLISH, "restorecon %s >/dev/null 2>/dev/null", appProcessCopy)); + postExecution.add(String.format(Locale.ENGLISH, "%s rm %s >/dev/null 2>/dev/null", BOX, appProcessCopy)); return appProcessCopy; } } diff --git a/librootjava/src/main/java/eu/chainfire/librootjava/Debugger.java b/librootjava/src/main/java/eu/chainfire/librootjava/Debugger.java new file mode 100644 index 0000000..27e2a31 --- /dev/null +++ b/librootjava/src/main/java/eu/chainfire/librootjava/Debugger.java @@ -0,0 +1,138 @@ +package eu.chainfire.librootjava; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.io.PrintStream; + +/** + * Utility methods to support debugging + */ +@SuppressWarnings({"unused", "WeakerAccess"}) +public class Debugger { + /** + * Is debugging enabled ? + */ + static volatile boolean enabled = false; + + /** + * Is debugging enabled ?
+ *
+ * If called from non-root, this will return if we are launching new processes with debugging + * enabled. If called from root, this will return if the current process was launched + * with debugging enabled. + * + * @return Debugging enabled + */ + public static boolean isEnabled() { + if (android.os.Process.myUid() >= 10000) { + return enabled; + } else { + return Reflection.isDebuggingEnabled(); + } + } + + /** + * Launch root processes with debugging enabled ? + *
+ * To prevent issues on release builds, BuildConfig.DEBUG should be respected. So instead + * of passing true you would pass BuildConfig.DEBUG, while false + * remains false. + * + * @param enabled Enable debugging (default: false) + */ + public static void setEnabled(boolean enabled) { + Debugger.enabled = enabled; + } + + /** + * Cache for name to present to debugger. Really only used to determine if we have manually + * set a name already. + */ + private static volatile String name = null; + + /** + * Set name to present to debugger
+ *
+ * This method should only be called from the process running as root.
+ *
+ * Debugging will not work if this method has not been called, but the + * {@link #waitFor(boolean)} method will call it for you, if used.
+ *
+ * {@link RootJava#restoreOriginalLdLibraryPath()} should have been called before calling + * this method.
+ *
+ * To prevent issues with release builds, this call should be wrapped in a BuildConfig.DEBUG + * check. + * + * @param name Name to present to debugger, or null to use process name + * + * @see #waitFor(boolean) + */ + public static void setName(String name) { + if (Debugger.name == null) { + if (name == null) { + final File cmdline = new File("/proc/" + android.os.Process.myPid() + "/cmdline"); + try (BufferedReader reader = new BufferedReader(new FileReader(cmdline))) { + name = reader.readLine(); + if (name.indexOf(' ') > 0) name = name.substring(0, name.indexOf(' ')); + if (name.indexOf('\0') > 0) name = name.substring(0, name.indexOf('\0')); + } catch (IOException e) { + name = "librootjava:unknown"; + } + } + Debugger.name = name; + Reflection.setAppName(name); + } + } + + /** + * Wait for debugger to connect
+ *
+ * This method should only be called from the process running as root.
+ *
+ * If {@link #setName(String)} has not been called manually, the display name for the + * debugger will be set to the current process name.
+ *
+ * After this method has been called, you can connect AndroidStudio's debugger to the root + * process via Run->Attach Debugger to Android process.
+ *
+ * {@link RootJava#restoreOriginalLdLibraryPath()} should have been called before calling + * this method.
+ *
+ * Android's internal debugger code will print to STDOUT during this call using System.println, + * which may be annoying if your non-root process communicates with the root process through + * STDIN/STDOUT/STDERR. If the swallowOutput parameter is set to true, System.println + * will be temporarily redirected, and reset back to STDOUT afterwards.
+ *
+ * To prevent issues with release builds, this call should be wrapped in a BuildConfig.DEBUG + * check: + * + *
+     * {@code
+     * if (BuildConfig.DEBUG) {
+     *     Debugger.waitFor(true);
+     * }
+     * }
+     * 
+ * + * @param swallowOutput Temporarily redirect STDOUT ? + */ + public static void waitFor(boolean swallowOutput) { + if (Reflection.isDebuggingEnabled()) { + if (swallowOutput) { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + System.setOut(new PrintStream(buffer)); + } + setName(null); + android.os.Debug.waitForDebugger(); + if (swallowOutput) { + System.setOut(new PrintStream(new FileOutputStream(FileDescriptor.out))); + } + } + } +} diff --git a/librootjava/src/main/java/eu/chainfire/librootjava/Logger.java b/librootjava/src/main/java/eu/chainfire/librootjava/Logger.java index 5ad1ee0..b2f81f9 100644 --- a/librootjava/src/main/java/eu/chainfire/librootjava/Logger.java +++ b/librootjava/src/main/java/eu/chainfire/librootjava/Logger.java @@ -23,7 +23,7 @@ @SuppressWarnings({"unused", "WeakerAccess"}) public class Logger { private static String getDefaultLogTag(){ - String tag = BuildConfig.APPLICATION_ID; + String tag = BuildConfig.LIBRARY_PACKAGE_NAME; int p; while ((p = tag.indexOf('.')) >= 0) { tag = tag.substring(p + 1); diff --git a/librootjava/src/main/java/eu/chainfire/librootjava/Reflection.java b/librootjava/src/main/java/eu/chainfire/librootjava/Reflection.java index 466d351..077d35b 100644 --- a/librootjava/src/main/java/eu/chainfire/librootjava/Reflection.java +++ b/librootjava/src/main/java/eu/chainfire/librootjava/Reflection.java @@ -23,7 +23,9 @@ class Reflection { private static Context systemContext = null; /** - * Stability: unlikely to change, this implementation works from 1.6 through 9.0 + * Retrieve system context
+ *
+ * Stability: unlikely to change, this implementation works from 1.6 through 9.0
* * @see RootJava#getSystemContext() * @@ -66,17 +68,17 @@ static Context getSystemContext() { private static Object oActivityManager = null; /** - * Retrieve ActivityManager instance without needing a context + * Retrieve ActivityManager instance without needing a context
+ *
+ * Stability: has changed before, might change again, rare * * @return ActivityManager */ @SuppressLint("PrivateApi") @SuppressWarnings({"JavaReflectionMemberAccess"}) private static Object getActivityManager() { - // We could possibly cast this to ActivityManager instead of Object, but we don't currently - // need that for our usage, and it would require retesting everything. Maybe ActivityManager - // is even wrong and it should be ActivityManagerService, for which we don't have the class - // definition anyway. TODO: investigate further. + // Return object is AIDL interface IActivityManager, not an ActivityManager or + // ActivityManagerService synchronized (lock) { if (oActivityManager != null) { @@ -110,7 +112,9 @@ private static Object getActivityManager() { private static Integer FLAG_RECEIVER_FROM_SHELL = null; /** - * Retrieve value of Intent.FLAG_RECEIVER_FROM_SHELL, if it exists + * Retrieve value of Intent.FLAG_RECEIVER_FROM_SHELL, if it exists
+ *
+ * Stability: stable, even if the flag goes away again this is unlikely to affect things * * @return FLAG_RECEIVER_FROM_SHELL or 0 */ @@ -169,8 +173,10 @@ private static Method getBroadcastIntent(Class cActivityManager) { } /** - * Stability: the implementation for this will definitely change over time - * + * Broadcast intent
+ *
+ * Stability: the implementation for this will definitely change over time
+ *
* This implementation does not require us to have a context * * @see RootJava#sendBroadcast(Intent) @@ -188,12 +194,12 @@ static void sendBroadcast(Intent intent) { Method mBroadcastIntent = getBroadcastIntent(oActivityManager.getClass()); if (mBroadcastIntent.getParameterTypes().length == 13) { // API 24+ - mBroadcastIntent.invoke(oActivityManager, null, intent, null, null, 0, null, null, null, -1, null, true, false, 0); + mBroadcastIntent.invoke(oActivityManager, null, intent, null, null, 0, null, null, null, -1, null, false, false, 0); return; } if (mBroadcastIntent.getParameterTypes().length == 12) { // API 21+ - mBroadcastIntent.invoke(oActivityManager, null, intent, null, null, 0, null, null, null, -1, true, false, 0); + mBroadcastIntent.invoke(oActivityManager, null, intent, null, null, 0, null, null, null, -1, false, false, 0); return; } } catch (Exception e) { @@ -205,6 +211,50 @@ static void sendBroadcast(Intent intent) { throw new RuntimeException("librootjava: unable to send broadcast"); } + /** + * Determine if debugging is enabled on the VM level
+ *
+ * Stability: unlikely to change, this implementation works from 1.6 through 9.0
+ * + * @see Debugger#isEnabled() + * + * @return Debugging enabled + */ + @SuppressLint("PrivateApi") + static boolean isDebuggingEnabled() { + try { + Class cVMDebug = Class.forName("dalvik.system.VMDebug"); + Method mIsDebuggingEnabled = cVMDebug.getDeclaredMethod("isDebuggingEnabled"); + return (Boolean)mIsDebuggingEnabled.invoke(null); + } catch (Exception e) { + Logger.ex(e); + return false; + } + } + + /** + * Set app name for debugger connection + *
+ * Stability: unlikely to change, this implementation works from 1.6 through 9.0
+ * + * @see Debugger#setName(String) + */ + @SuppressLint("PrivateApi") + static void setAppName(String name) { + try { + Class cDdmHandleAppName = Class.forName("android.ddm.DdmHandleAppName"); + Method m = cDdmHandleAppName.getDeclaredMethod("setAppName", String.class, int.class); + m.invoke(null, name, 0); + } catch (Exception e) { + Logger.ex(e); + } + } + + /** + * Internal class to retrieve an interface from a Binder (Proxy) + * + * @param Interface + */ @SuppressWarnings("unchecked") static class InterfaceRetriever { /** diff --git a/librootjava/src/main/java/eu/chainfire/librootjava/RootIPC.java b/librootjava/src/main/java/eu/chainfire/librootjava/RootIPC.java index 3bbe753..3d6fab6 100644 --- a/librootjava/src/main/java/eu/chainfire/librootjava/RootIPC.java +++ b/librootjava/src/main/java/eu/chainfire/librootjava/RootIPC.java @@ -149,6 +149,7 @@ public void broadcastIPC() { Intent intent = new Intent(); intent.setPackage(packageName); intent.setAction(RootIPCReceiver.BROADCAST_ACTION); + intent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND); Bundle bundle = new Bundle(); bundle.putBinder(RootIPCReceiver.BROADCAST_BINDER, binder); diff --git a/librootjava/src/main/java/eu/chainfire/librootjava/RootJava.java b/librootjava/src/main/java/eu/chainfire/librootjava/RootJava.java index f50f58f..826d7c8 100644 --- a/librootjava/src/main/java/eu/chainfire/librootjava/RootJava.java +++ b/librootjava/src/main/java/eu/chainfire/librootjava/RootJava.java @@ -21,8 +21,10 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.os.Build; +import android.os.SystemClock; import java.io.File; +import java.io.FilenameFilter; import java.util.ArrayList; import java.util.List; import java.util.Locale; @@ -52,6 +54,7 @@ * @see #getSystemContext() * @see #getPackageContext(String) * @see #getLibraryPath(Context, String) + * @see Debugger#setEnabled(boolean) */ @SuppressWarnings({"unused", "WeakerAccess", "Convert2Diamond"}) public class RootJava { @@ -182,10 +185,15 @@ public static String getLaunchString(String packageCodePath, String clazz, Strin if (niceName != null) { extraParams += " --nice-name=" + niceName; } - //TODO debugging; the next line is successful in creating a jdwp server on Pie, and it's listed in 'adb jdwp', but connecting to it seemingly does nothing - //vmParams += " -XjdwpProvider:adbconnection -XjdwpOptions:transport=dt_android_adb,suspend=n,server=y"; - //vmParams += " -agentlib:jdwp=transport=dt_android_adb,suspend=n,server=y"; - // android.os.Debug.waitForDebugger(); can be used in root's Main + if (Debugger.enabled) { // we don't use isEnabled() because that has a different meaning when called as root, and though rare we might call this method from root too + vmParams += " -Xcompiler-option --debuggable"; + if (Build.VERSION.SDK_INT >= 28) { + // Android 9.0 Pie changed things up a bit + vmParams += " -XjdwpProvider:internal -XjdwpOptions:transport=dt_android_adb,suspend=n,server=y"; + } else { + vmParams += " -agentlib:jdwp=transport=dt_android_adb,suspend=n,server=y"; + } + } String ret = String.format("NO_ADDR_COMPAT_LAYOUT_FIXUP=1 %sCLASSPATH=%s %s%s /system/bin%s %s", prefix.toString(), packageCodePath, app_process, vmParams, extraParams, clazz); if (params != null) { StringBuilder full = new StringBuilder(ret); @@ -241,6 +249,69 @@ public static List getLaunchScript(Context context, Class clazz, Stri return script; } + /** Prefixes of filename to remove from the app's cache directory */ + public static final String[] CLEANUP_CACHE_PREFIXES = new String[] { ".app_process32_", ".app_process64_" }; + + /** + * Clean up leftover files from our cache directory.
+ *
+ * In ideal circumstances no files should be left dangling, but in practise it happens sooner + * or later anyway. Periodically (once per app launch or per boot) calling this method is + * advised.
+ *
+ * This method should be called from a background thread, as it performs disk i/o.
+ *
+ * It is difficult to determine which of these files may actually be in use, especially in + * daemon mode. We try to determine device boot time, and wipe everything from before that + * time. For safety we explicitly keep files using our current UUID. + * + * @param context Context to retrieve cache directory from + */ + public static void cleanupCache(Context context) { + cleanupCache(context, CLEANUP_CACHE_PREFIXES); + } + + /** + * Clean up leftover files from our cache directory.
+ *
+ * This version is for internal use, see {@link #cleanupCache(Context)} instead. + * + * @param context Context to retrieve cache directory from + * @param prefixes List of prefixes to scrub + */ + public static void cleanupCache(Context context, final String[] prefixes) { + try { + File cacheDir = context.getCacheDir(); + if (cacheDir.exists()) { + // determine time of last boot + long boot = System.currentTimeMillis() - SystemClock.elapsedRealtime(); + + // find our files + for (File file : cacheDir.listFiles(new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + boolean accept = false; + for (String prefix : prefixes) { + // just in case: don't return files that contain our current uuid + if (name.startsWith(prefix) && !name.endsWith(AppProcess.UUID)) { + accept = true; + break; + } + } + return accept; + } + })) { + if (file.lastModified() < boot) { + //noinspection ResultOfMethodCallIgnored + file.delete(); + } + } + } + } catch (Exception e) { + Logger.ex(e); + } + } + // ------------------------ calls for root ------------------------ /** diff --git a/librootjava_example/build.gradle b/librootjava_example/build.gradle index c98df69..fdf499e 100644 --- a/librootjava_example/build.gradle +++ b/librootjava_example/build.gradle @@ -2,7 +2,7 @@ apply plugin: 'com.android.application' android { compileSdkVersion 26 - buildToolsVersion '28.0.3' + buildToolsVersion '30.0.3' defaultConfig { applicationId "eu.chainfire.librootjava_example" @@ -27,11 +27,16 @@ dependencies { implementation 'com.android.support:appcompat-v7:26.1.0' implementation 'com.android.support.constraint:constraint-layout:1.1.3' - implementation 'eu.chainfire:libsuperuser:1.0.0.+' + implementation 'eu.chainfire:libsuperuser:1.1.1' - /* Uncomment this line and commment the other one during development to use the local maven - repository version, installed by the installMavenLocal Gradle task */ - //implementation('eu.chainfire:librootjava:1.0.0-SNAPSHOT') { changing = true } + // --- librootjava dependency --- - implementation 'eu.chainfire:librootjava:1.0.0' + /* Use module sources directly */ + //implementation project(':librootjava') + + /* Use local Maven repository version, installed by installMavenLocal Gradle task */ + //implementation('eu.chainfire:librootjava:1.3.3-SNAPSHOT') { changing = true } + + /* Use jitpack version */ + implementation 'eu.chainfire:librootjava:1.3.3' } diff --git a/librootjava_example/src/main/java/eu/chainfire/librootjava_example/MainActivity.java b/librootjava_example/src/main/java/eu/chainfire/librootjava_example/MainActivity.java index 0cf003c..9a3a59d 100644 --- a/librootjava_example/src/main/java/eu/chainfire/librootjava_example/MainActivity.java +++ b/librootjava_example/src/main/java/eu/chainfire/librootjava_example/MainActivity.java @@ -34,6 +34,7 @@ import eu.chainfire.librootjava.Logger; import eu.chainfire.librootjava.RootIPCReceiver; +import eu.chainfire.librootjava.RootJava; import eu.chainfire.librootjava_example.root.IIPC; import eu.chainfire.librootjava_example.root.IPingCallback; import eu.chainfire.librootjava_example.root.PassedData; @@ -80,6 +81,15 @@ it inside a method (in this example), the context passed to its constructor is a wrapper. We need to update it to a proper context, so we may actually receive the Binder object from our root code. */ ipcReceiver.setContext(this); + + /* Cleanup leftover files from our cache directory. This is not exactly an elegant way to + do it, but it illustrates that this should be done off of the main UI thread. */ + (new Thread(new Runnable() { + @Override + public void run() { + RootJava.cleanupCache(MainActivity.this); + } + })).start(); } @Override diff --git a/librootjavadaemon/CMakeLists.txt b/librootjavadaemon/CMakeLists.txt index 7b48e96..b872e6c 100644 --- a/librootjavadaemon/CMakeLists.txt +++ b/librootjavadaemon/CMakeLists.txt @@ -1,3 +1,8 @@ cmake_minimum_required(VERSION 3.4.1) + add_executable( libdaemonize.so src/main/jni/daemonize.cpp ) + +target_link_libraries( libdaemonize.so log ) + +#target_compile_definitions( libdaemonize.so PRIVATE DEBUG ) diff --git a/librootjavadaemon/README.md b/librootjavadaemon/README.md index dedda54..c2f5496 100644 --- a/librootjavadaemon/README.md +++ b/librootjavadaemon/README.md @@ -1,5 +1,7 @@ # libRootJavaDaemon +[![](https://jitpack.io/v/eu.chainfire/librootjava.svg)](https://jitpack.io/#eu.chainfire/librootjava) + Add-on for [libRootJava](../librootjava) to run the root process as a daemon. @@ -61,7 +63,7 @@ statements (generally after setting up logging): // If a daemon of the same version is already running, this // call will trigger process termination, and will thus // never return. - RootDaemon.daemonize(BuildConfig.APPLICATION_ID, 0); + RootDaemon.daemonize(BuildConfig.APPLICATION_ID, 0, false, null); // ... @@ -113,18 +115,29 @@ them with your own handling. #### Termination This daemon process will only terminate when explicitly told to do so, -either through IPC or a Linux kill signal (or if an unhandled -exception occurs). This is why in the example above we add a +either through IPC, a Linux kill signal, if an unhandled +exception occurs, or (if so configured) when the Android framework +dies. This is why in the example above we add a ```terminate()``` method to our IPC interface which calls ```RootDaemon.exit()```. This way you can tell the daemon to stop running from your non-root app through IPC. Note that this method will always trigger a ```RemoteException``` on the -non-root end when called. +non-root end when called through IPC. See the [example project](../librootjavadaemon_example) for further details. +#### Cleanup + +As with running code as root in normal (non-daemon) mode, files may need +to be created in our app's cache directory. The chances of leftover +files are actually higher in daemon mode, and the number of files is +higher too. + +To clean up, call ```RootDaemon.cleanupCache()``` instead of +```RootJava.cleanupCache()```. It is *not* needed to call both. + ## abiFilters This library includes native code for all platforms the NDK supports. @@ -139,8 +152,23 @@ on the Android site. ## Gradle +Root `build.gradle`: + +``` +allprojects { + repositories { + ... + maven { url 'https://jitpack.io' } + } +} +``` + +Module `build.gradle`: + ``` -implementation 'eu.chainfire:librootjavadaemon:1.0.0' +dependencies { + implementation 'eu.chainfire.librootjava:librootjavadaemon:1.3.3' +} ``` You should update to the latest libRootJava and libRootJavaDaemon at the diff --git a/librootjavadaemon/build.gradle b/librootjavadaemon/build.gradle index 1fed488..5e9a168 100644 --- a/librootjavadaemon/build.gradle +++ b/librootjavadaemon/build.gradle @@ -1,10 +1,9 @@ apply plugin: 'com.android.library' apply plugin: 'com.github.dcendents.android-maven' -apply plugin: 'com.jfrog.bintray' android { - compileSdkVersion 26 - buildToolsVersion '28.0.3' + compileSdkVersion 30 + buildToolsVersion '30.0.3' defaultConfig { minSdkVersion 21 targetSdkVersion 26 @@ -23,11 +22,14 @@ android { } dependencies { - /* Uncomment this line and commment the other one during development to use the local maven - repository version, installed by the installMavenLocal Gradle task */ - //implementation('eu.chainfire:librootjava:1.0.0-SNAPSHOT') { changing = true } + /* Use module sources directly */ + //implementation project(':librootjava') - implementation 'eu.chainfire:librootjava:1.0.0' + /* Use local Maven repository version, installed by installMavenLocal Gradle task */ + //implementation('eu.chainfire:librootjava:1.3.3-SNAPSHOT') { changing = true } + + /* Use jitpack version */ + implementation 'eu.chainfire.librootjava:librootjava:1.3.3' } task sourcesJar(type: Jar) { @@ -61,7 +63,7 @@ ext { gitUrl = 'https://github.com/Chainfire/librootjava.git' issueTrackerUrl = 'https://github.com/Chainfire/librootjava/issues' - libraryVersion = '1.0.0' + libraryVersion = '1.3.3' developerId = 'Chainfire' developerName = 'Jorrit Jongma' @@ -70,9 +72,6 @@ ext { licenseName = 'The Apache Software License, Version 2.0' licenseUrl = 'http://www.apache.org/licenses/LICENSE-2.0.txt' allLicenses = ["Apache-2.0"] - - bintrayRepo = 'maven' - bintrayName = artifact } task installMavenLocal(type: Upload) { @@ -82,42 +81,14 @@ task installMavenLocal(type: Upload) { packaging 'aar' groupId = publishedGroupId artifactId = artifact - version = '1.0.0-SNAPSHOT' + version = libraryVersion + '-SNAPSHOT' } } } -// Workaround bintray bug ignoring these from pom and bintray settings version = libraryVersion group = publishedGroupId -bintray { - Properties properties = new Properties() - properties.load(project.rootProject.file('local.properties').newDataInputStream()) - user = properties.getProperty('bintray.user') - key = properties.getProperty('bintray.apikey') - - configurations = ['archives'] - dryRun = false - publish = true - pkg { - repo = bintrayRepo - name = libraryName - desc = libraryDescription - websiteUrl = siteUrl - issueTrackerUrl = issueTrackerUrl // doesn't actually work? - vcsUrl = gitUrl - //githubRepo = gitUrl // some more bintray weirdness here, breaks upload - //githubReleaseNotesFile = 'README.md' - licenses = allLicenses - publicDownloadNumbers = true - version { - name = libraryVersion - released = new Date() - } - } -} - install { repositories.mavenInstaller { pom.project { @@ -148,5 +119,3 @@ install { } } } - -bintrayUpload.dependsOn install diff --git a/librootjavadaemon/src/main/java/eu/chainfire/librootjavadaemon/RootDaemon.java b/librootjavadaemon/src/main/java/eu/chainfire/librootjavadaemon/RootDaemon.java index b804faf..f122409 100644 --- a/librootjavadaemon/src/main/java/eu/chainfire/librootjavadaemon/RootDaemon.java +++ b/librootjavadaemon/src/main/java/eu/chainfire/librootjavadaemon/RootDaemon.java @@ -36,7 +36,7 @@ * Class with utility function sto launch Java code running as a root as a daemon * * @see #getLaunchScript(Context, Class, String, String, String[], String) - * @see #daemonize(String, int) + * @see #daemonize(String, int, boolean, OnExitListener) * @see #run() * @see #exit() */ @@ -73,15 +73,27 @@ public static List patchLaunchScript(Context context, List scrip // patch the main script line String app_process_path = app_process.substring(0, app_process.lastIndexOf('/')); - // copy our executable - String libSrc = RootJava.getLibraryPath(context, "daemonize"); - String libDest = app_process_path + "/.daemonize_" + AppProcess.uuid; - ret.add(String.format(Locale.ENGLISH, "%s cp %s %s >/dev/null 2>/dev/null", AppProcess.box, libSrc, libDest)); - ret.add(String.format(Locale.ENGLISH, "%s chmod 0700 %s >/dev/null 2>/dev/null", AppProcess.box, libDest)); + // our executable + String libSource = RootJava.getLibraryPath(context, "daemonize"); + String libExec; + + if (app_process_path.startsWith("/system/bin")) { + // app_process was not relocated, assume caller knows what he's doing, and + // run our executable from its library location + libExec = libSource; + } else { + // copy our executable + libExec = app_process_path + "/.daemonize_" + AppProcess.UUID; + boolean onData = libExec.startsWith("/data/"); + + ret.add(String.format(Locale.ENGLISH, "%s cp %s %s >/dev/null 2>/dev/null", AppProcess.BOX, libSource, libExec)); + ret.add(String.format(Locale.ENGLISH, "%s chmod %s %s >/dev/null 2>/dev/null", AppProcess.BOX, onData ? "0766" : "0700", libExec)); + if (onData) ret.add(String.format(Locale.ENGLISH, "restorecon %s >/dev/null 2>/dev/null", libExec)); + } // inject executable into command int idx = line.indexOf(app_process); - ret.add(line.substring(0, idx) + libDest + " " + line.substring(idx)); + ret.add(line.substring(0, idx) + libExec + " " + line.substring(idx)); in_post = true; } else if (in_post && line.contains("box rm")) { @@ -114,11 +126,36 @@ public static List getLaunchScript(Context context, Class clazz, Stri return patchLaunchScript(context, RootJava.getLaunchScript(context, clazz, app_process, relocate_path, params, niceName)); } + /** Prefixes of filename to remove from the app's cache directory */ + public static final String[] CLEANUP_CACHE_PREFIXES = new String[] { ".daemonize_" }; + + /** + * Clean up leftover files from our cache directory.
+ *
+ * Call this method instead of (not in addition to) RootJava#cleanupCache(Context). + * + * @param context Context to retrieve cache directory from + */ + public static void cleanupCache(Context context) { + String[] prefixes = new String[RootJava.CLEANUP_CACHE_PREFIXES.length + CLEANUP_CACHE_PREFIXES.length]; + System.arraycopy(RootJava.CLEANUP_CACHE_PREFIXES, 0, prefixes, 0, RootJava.CLEANUP_CACHE_PREFIXES.length); + System.arraycopy(CLEANUP_CACHE_PREFIXES, 0, prefixes, RootJava.CLEANUP_CACHE_PREFIXES.length, CLEANUP_CACHE_PREFIXES.length); + RootJava.cleanupCache(context, prefixes); + } + // ------------------------ calls for root ------------------------ /** Registered interfaces */ private static final List ipcs = new ArrayList(); + /** Called before termination */ + public interface OnExitListener { + void onExit(); + } + + /** Stored by daemonize(), called by exit() */ + private static volatile OnExitListener onExitListener = null; + /** * Makes sure there is only a single daemon running with this code parameter. This should * be one of the first calls in your process to be run as daemon, just after setting up logging @@ -142,10 +179,12 @@ public static List getLaunchScript(Context context, Class clazz, Stri * * @param packageName Package name of the app. BuildConfig.APPLICATION_ID can generally be used. * @param code User-value, should be unique per daemon process + * @param surviveFrameworkRestart If false (recommended), automatically terminate if the Android framework restarts + * @param exitListener Callback called before the daemon exists either due to a newer daemon version being started or {@link #exit()} being called, or null */ @SuppressLint("PrivateApi") - public static void daemonize(String packageName, int code) { - String id = packageName + "#" + String.valueOf(code) + "#daemonize"; + public static void daemonize(String packageName, int code, boolean surviveFrameworkRestart, OnExitListener exitListener) { + String id = packageName + "_" + String.valueOf(code) + "_daemonize"; File apk = new File(System.getenv("CLASSPATH")); final String version = String.format(Locale.ENGLISH, "%s:%d:%d", apk.getAbsolutePath(), apk.lastModified(), apk.length()); @@ -170,12 +209,39 @@ public static void daemonize(String packageName, int code) { } else { Logger.dp(LOG_PREFIX, "Service already running, requesting re-broadcast and aborting"); ipc.broadcast(); - System.exit(0); + exit(); } } // If we reach this, there either was no previous daemon, or it was outdated - Logger.ep(LOG_PREFIX, "Installing service"); + Logger.dp(LOG_PREFIX, "Installing service"); + onExitListener = exitListener; + + if (!surviveFrameworkRestart) { + /* We link to Android's activity service, which lives in system_server. If the + framework is restarted, for example through stop/start in a root shell, this + service will die and we will be notified. + + Obviously when setting surviveFrameworkRestart to true, things you do in + your own code may still cause this process to terminate when the framework + dies, we're just not doing it automatically. */ + + IBinder activityService = (IBinder)mGetService.invoke(null, Context.ACTIVITY_SERVICE); + if (activityService != null) { + try { + activityService.linkToDeath(new IBinder.DeathRecipient() { + @Override + public void binderDied() { + exit(); + } + }, 0); + } catch (RemoteException e) { + // already dead + exit(); + } + } + } + mAddService.invoke(null, id, new IRootDaemonIPC.Stub() { @Override public String getVersion() { @@ -208,7 +274,7 @@ public void broadcast() { *
* Use this method instead of librootjava's 'new RootIPC()' constructor when running as daemon. * - * @param packageName Package name of the app. Use the same value as used when calling {@link #daemonize(String, int)}. + * @param packageName Package name of the app. Use the same value as used when calling {@link #daemonize(String, int, boolean, OnExitListener)}. * @param ipc Binder object to wrap and send out * @param code User-value, should be unique per Binder * @return RootIPC instance @@ -236,7 +302,8 @@ public static RootIPC register(String packageName, IBinder ipc, int code) { * the main() implementation. The initial Binder broadcasts and the connections themselves * are handled in background threads created by the RootIPC instances created when * {@link #register(String, IBinder, int)} is called, and re-broadcasting those interfaces - * is done by the internal Binder interface registered by {@link #daemonize(String, int)}.
+ * is done by the internal Binder interface registered by + * {@link #daemonize(String, int, boolean, OnExitListener)}.
*
* This method never returns! */ @@ -260,15 +327,23 @@ public static void run() { * RemoteException on the other end. */ public static void exit() { + /* We do not return from the run() call but immediately exit, so if this method is called + from inside a Binder interface method implementation, the process has died before the + IPC call to terminate completes on the other end. This triggers a RemoteException so the + other end can easily verify this process has terminated. It also prevents a + race-condition between the old service dying and new service registering. Additionally it + saves us from having to use another synchronizer to cope with a termination request + coming in from another daemon launch before run() is actually called. */ + Logger.dp(LOG_PREFIX, "Exiting"); - /* We do not return from the run() call but immediately exit, so the process has died - before the IPC call to terminate completes on the other end. This triggers a - RemoteException so the other end can easily verify this process has terminated. It - also prevents a race-condition between the old service dying and new service registering. - Additionally it saves us from having to use another synchronizer to cope with a - termination request coming in from another daemon launch before run() is actually - called. */ + try { + if (onExitListener != null) { + onExitListener.onExit(); + } + } catch (Exception e) { + Logger.ex(e); + } try { /* Unlike when using RootJava.getLaunchScript(), RootDaemon.getLaunchScript() does @@ -278,6 +353,16 @@ not clean up our relocated app_process binary after executing (which might preve if (app_process.exists() && !app_process.getAbsolutePath().startsWith("/system/bin/")) { //noinspection ResultOfMethodCallIgnored app_process.delete(); + + // See if we can also find a copy of the daemonize binary + String daemonize_path = app_process.getAbsolutePath(); + daemonize_path = daemonize_path.replace(".app_process32_", ".daemonize_"); + daemonize_path = daemonize_path.replace(".app_process64_", ".daemonize_"); + File daemonize = new File(daemonize_path); + if (daemonize.exists()) { + //noinspection ResultOfMethodCallIgnored + daemonize.delete(); + } } } catch (IOException e) { // should never actually happen diff --git a/librootjavadaemon/src/main/jni/daemonize.cpp b/librootjavadaemon/src/main/jni/daemonize.cpp index cf56118..eca7655 100644 --- a/librootjavadaemon/src/main/jni/daemonize.cpp +++ b/librootjavadaemon/src/main/jni/daemonize.cpp @@ -21,12 +21,33 @@ #include #include #include +#include +#include + +#ifdef DEBUG +#include +#define LOG(...) ((void)__android_log_print(ANDROID_LOG_DEBUG, "libdaemonize", __VA_ARGS__)) +#else +#define LOG(...) ((void)0) +#endif + +int sleep_ms(int ms) { + struct timespec ts; + ts.tv_sec = ms / 1000; + ts.tv_nsec = (ms % 1000) * 1000000; + if ((nanosleep(&ts,&ts) == -1) && (errno == EINTR)) { + int ret = (ts.tv_sec * 1000) + (ts.tv_nsec / 1000000); + if (ret < 1) ret = 1; + return ret; + } + return 0; +} /* Proper daemonization includes forking, closing the current STDIN/STDOUT/STDERR, creating a new * session, and forking again, making sure the twice-forked process becomes a child of init (1) */ static int fork_daemon(int returnParent) { pid_t child = fork(); - if (child == 0) { + if (child == 0) { // 1st child close(STDIN_FILENO); close(STDOUT_FILENO); close(STDERR_FILENO); @@ -39,12 +60,28 @@ static int fork_daemon(int returnParent) { setsid(); pid_t child2 = fork(); - if (child2 <= 0) return 0; // success child or error on 2nd fork - exit(EXIT_SUCCESS); + if (child2 == 0) { // 2nd child + return 0; // return execution to caller + } else if (child2 > 0) { // 1st child, fork ok + exit(EXIT_SUCCESS); + } else if (child2 < 0) { // 1st child, fork fail + LOG("2nd fork failed (%d)", errno); + exit(EXIT_FAILURE); + } + } + + // parent + if (child < 0) { + LOG("1st fork failed (%d)", errno); + return -1; // error on 1st fork + } + while (true) { + int status; + pid_t waited = waitpid(child, &status, 0); + if ((waited == child) && WIFEXITED(status)) { + break; + } } - if (child < 0) return -1; // error on 1st fork - int status; - waitpid(child, &status, 0); if (!returnParent) exit(EXIT_SUCCESS); return 1; // success parent } @@ -52,8 +89,17 @@ static int fork_daemon(int returnParent) { extern "C" { int main(int argc, char *argv[], char** envp) { - if (fork_daemon(0) == 0) { - execv(argv[1], &argv[1]); + if (fork_daemon(0) == 0) { // daemonized child + // On some devices in the early boot stages, execv will fail with EACCESS, cause unknown. + // Retrying a couple of times seems to work. Most-seen required attempts is three. + // That retrying works implies some sort of race-condition, possibly SELinux related. + for (int i = 0; i < 16; i++) { + execv(argv[1], &argv[1]); // never returns if successful + LOG("[%d] execv(%s, ...)-->%d", i, argv[1], errno); + sleep_ms(16); + } + LOG("too many failures, aborting"); + exit(EXIT_FAILURE); } } diff --git a/librootjavadaemon_example/build.gradle b/librootjavadaemon_example/build.gradle index 435d320..af9981a 100644 --- a/librootjavadaemon_example/build.gradle +++ b/librootjavadaemon_example/build.gradle @@ -2,7 +2,7 @@ apply plugin: 'com.android.application' android { compileSdkVersion 26 - buildToolsVersion '28.0.3' + buildToolsVersion '30.0.3' defaultConfig { applicationId "eu.chainfire.librootjavadaemon_example" @@ -27,13 +27,19 @@ dependencies { implementation 'com.android.support:appcompat-v7:26.1.0' implementation 'com.android.support.constraint:constraint-layout:1.1.3' - implementation 'eu.chainfire:libsuperuser:1.0.0.+' + implementation 'eu.chainfire:libsuperuser:1.1.1' - /* Uncomment these lines and commment the other ones during development to use the local maven - repository version, installed by the installMavenLocal Gradle task */ - //implementation('eu.chainfire:librootjava:1.0.0-SNAPSHOT') { changing = true } - //implementation('eu.chainfire:librootjavadaemon:1.0.0-SNAPSHOT') { changing = true } + // --- librootjava and librootjavadaemon dependencies --- - implementation 'eu.chainfire:librootjava:1.0.0' - implementation 'eu.chainfire:librootjavadaemon:1.0.0' + /* Use module sources directly */ + //implementation project(':librootjava') + //implementation project(':librootjavadaemon') + + /* Use local Maven repository version, installed by installMavenLocal Gradle task */ + //implementation('eu.chainfire:librootjava:1.3.3-SNAPSHOT') { changing = true } + //implementation('eu.chainfire:librootjavadaemon:1.3.3-SNAPSHOT') { changing = true } + + /* Use jitpack version */ + implementation 'eu.chainfire:librootjava:1.3.3' + implementation 'eu.chainfire.librootjava:librootjavadaemon:1.3.3' } diff --git a/librootjavadaemon_example/src/main/java/eu/chainfire/librootjavadaemon_example/MainActivity.java b/librootjavadaemon_example/src/main/java/eu/chainfire/librootjavadaemon_example/MainActivity.java index 0085446..85fa712 100644 --- a/librootjavadaemon_example/src/main/java/eu/chainfire/librootjavadaemon_example/MainActivity.java +++ b/librootjavadaemon_example/src/main/java/eu/chainfire/librootjavadaemon_example/MainActivity.java @@ -28,6 +28,7 @@ import eu.chainfire.librootjava.Logger; import eu.chainfire.librootjava.RootIPCReceiver; +import eu.chainfire.librootjavadaemon.RootDaemon; import eu.chainfire.librootjavadaemon_example.root.IIPC; import eu.chainfire.librootjavadaemon_example.root.RootMain; import eu.chainfire.libsuperuser.Debug; @@ -69,7 +70,15 @@ protected void onCreate(Bundle savedInstanceState) { textView = findViewById(R.id.textView); textView.setHorizontallyScrolling(true); textView.setMovementMethod(new ScrollingMovementMethod()); + + // See librootjava's example for further commentary on these two calls ipcReceiver.setContext(this); + (new Thread(new Runnable() { + @Override + public void run() { + RootDaemon.cleanupCache(MainActivity.this); + } + })).start(); } @Override diff --git a/librootjavadaemon_example/src/main/java/eu/chainfire/librootjavadaemon_example/root/RootMain.java b/librootjavadaemon_example/src/main/java/eu/chainfire/librootjavadaemon_example/root/RootMain.java index e036fb9..d2db3fa 100644 --- a/librootjavadaemon_example/src/main/java/eu/chainfire/librootjavadaemon_example/root/RootMain.java +++ b/librootjavadaemon_example/src/main/java/eu/chainfire/librootjavadaemon_example/root/RootMain.java @@ -91,7 +91,7 @@ public void uncaughtException(Thread thread, Throwable throwable) { */ private void run(String[] args) { // Become the daemon - RootDaemon.daemonize(BuildConfig.APPLICATION_ID, 0); + RootDaemon.daemonize(BuildConfig.APPLICATION_ID, 0, false, null); // Restore original LD_LIBRARY_PATH RootJava.restoreOriginalLdLibraryPath();