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/#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();