Multiuser daemon service support

This commit is contained in:
topjohnwu 2022-02-25 05:03:20 -08:00
parent 07b34a3802
commit 99e9b1deed
14 changed files with 519 additions and 359 deletions

View File

@ -20,6 +20,8 @@ import android.annotation.SuppressLint;
import android.content.Context;
import android.content.ContextWrapper;
import android.os.Build;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
import androidx.annotation.RestrictTo;
@ -34,6 +36,10 @@ import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
@RestrictTo(RestrictTo.Scope.LIBRARY)
public final class Utils {
@ -117,4 +123,20 @@ public final class Utils {
}
return total;
}
static <K, V> Map<K, V> newArrayMap() {
if (Build.VERSION.SDK_INT >= 19) {
return new ArrayMap<>();
} else {
return new HashMap<>();
}
}
static <E> Set<E> newArraySet() {
if (Build.VERSION.SDK_INT >= 23) {
return new ArraySet<>();
} else {
return new HashSet<>();
}
}
}

View File

@ -6,5 +6,5 @@ package com.topjohnwu.libsuexample;
interface ITestService {
int getPid();
int getUid();
String readCmdline();
String getUUID();
}

View File

@ -16,23 +16,9 @@
#include <jni.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
extern "C" JNIEXPORT JNICALL
jint Java_com_topjohnwu_libsuexample_AIDLService_nativeGetUid(
JNIEnv *env, jobject instance) {
return getuid();
}
extern "C" JNIEXPORT JNICALL
jstring Java_com_topjohnwu_libsuexample_AIDLService_nativeReadFile(
JNIEnv *env, jobject instance, jstring name) {
const char *path = env->GetStringUTFChars(name, nullptr);
int fd = open(path, O_RDONLY);
env->ReleaseStringUTFChars(name, path);
char buf[4096];
buf[read(fd, buf, sizeof(buf) - 1)] = 0;
return env->NewStringUTF(buf);
}

View File

@ -16,6 +16,8 @@
package com.topjohnwu.libsuexample;
import static com.topjohnwu.libsuexample.MainActivity.TAG;
import android.content.Intent;
import android.os.IBinder;
import android.os.Process;
@ -25,10 +27,11 @@ import androidx.annotation.NonNull;
import com.topjohnwu.superuser.ipc.RootService;
import static com.topjohnwu.libsuexample.MainActivity.TAG;
import java.util.UUID;
// Demonstrate RootService using AIDL (daemon mode)
class AIDLService extends RootService {
static {
// Only load the library when this class is loaded in a root process.
// The classloader will load this class (and call this static block) in the non-root
@ -40,7 +43,6 @@ class AIDLService extends RootService {
// Demonstrate we can also run native code via JNI with RootServices
native int nativeGetUid();
native String nativeReadFile(String file);
class TestIPC extends ITestService.Stub {
@Override
@ -54,12 +56,19 @@ class AIDLService extends RootService {
}
@Override
public String readCmdline() {
// Normally we cannot read /proc/cmdline without root
return nativeReadFile("/proc/cmdline");
public String getUUID() {
return uuid;
}
}
private String uuid;
@Override
public void onCreate() {
uuid = UUID.randomUUID().toString();
Log.d(TAG, "AIDLService: onCreate, " + uuid);
}
@Override
public void onRebind(@NonNull Intent intent) {
// This callback will be called when we are reusing a previously started root process
@ -75,7 +84,7 @@ class AIDLService extends RootService {
@Override
public boolean onUnbind(@NonNull Intent intent) {
Log.d(TAG, "AIDLService: onUnbind, client process unbound");
// We return true here to tell libsu that we want this service to run as a daemon
// Return true here so onRebindg will be called
return true;
}
}

View File

@ -31,19 +31,24 @@ import android.util.Log;
import androidx.annotation.NonNull;
import com.topjohnwu.superuser.internal.Utils;
import com.topjohnwu.superuser.ipc.RootService;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.UUID;
// Demonstrate root service using Messengers
class MSGService extends RootService implements Handler.Callback {
static final int MSG_GETINFO = 1;
static final int MSG_STOP = 2;
static final String CMDLINE_KEY = "cmdline";
static final String UUID_KEY = "uuid";
private String uuid;
@Override
public void onCreate() {
uuid = UUID.randomUUID().toString();
Log.d(TAG, "MSGService: onCreate, " + uuid);
}
@Override
public IBinder onBind(@NonNull Intent intent) {
@ -65,15 +70,8 @@ class MSGService extends RootService implements Handler.Callback {
reply.what = msg.what;
reply.arg1 = Process.myPid();
reply.arg2 = Process.myUid();
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (FileInputStream in = new FileInputStream("/proc/cmdline")) {
// libsu internal util method, pumps input to output
Utils.pump(in, out);
} catch (IOException e) {
Log.e(TAG, "IO error", e);
}
Bundle data = new Bundle();
data.putString(CMDLINE_KEY, out.toString());
data.putString(UUID_KEY, uuid);
reply.setData(data);
try {
msg.replyTo.send(reply);
@ -86,7 +84,7 @@ class MSGService extends RootService implements Handler.Callback {
@Override
public boolean onUnbind(@NonNull Intent intent) {
Log.d(TAG, "MSGService: onUnbind, client process unbound");
// Default returns false, which means NOT daemon mode
// Default returns false, which means onRebind will not be called
return false;
}
}

View File

@ -41,7 +41,6 @@ import com.topjohnwu.superuser.ipc.RootService;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.List;
public class MainActivity extends Activity implements Handler.Callback {
@ -57,15 +56,10 @@ public class MainActivity extends Activity implements Handler.Callback {
);
}
private ITestService testIPC;
private Messenger remoteMessenger;
private Messenger myMessenger = new Messenger(new Handler(Looper.getMainLooper(), this));
private MSGConnection conn = new MSGConnection();
private boolean daemonTestQueued = false;
private boolean serviceTestQueued = false;
private final Messenger me = new Messenger(new Handler(Looper.getMainLooper(), this));
private final List<String> consoleList = new AppendCallbackList();
private ActivityMainBinding binding;
private List<String> consoleList = new AppendCallbackList();
// Demonstrate Shell.Initializer
static class ExampleInitializer extends Shell.Initializer {
@ -79,135 +73,157 @@ public class MainActivity extends Activity implements Handler.Callback {
}
}
private AIDLConnection aidlConn;
private AIDLConnection daemonConn;
class AIDLConnection implements ServiceConnection {
private final boolean isDaemon;
AIDLConnection(boolean b) {
isDaemon = b;
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Log.d(TAG, "daemon onServiceConnected");
testIPC = ITestService.Stub.asInterface(service);
if (daemonTestQueued) {
daemonTestQueued = false;
testDaemon();
Log.d(TAG, "AIDL onServiceConnected");
if (isDaemon) daemonConn = this;
else aidlConn = this;
refreshUI();
ITestService ipc = ITestService.Stub.asInterface(service);
try {
consoleList.add("AIDL PID : " + ipc.getPid());
consoleList.add("AIDL UID : " + ipc.getUid());
consoleList.add("AIDL UUID: " + ipc.getUUID());
} catch (RemoteException e) {
Log.e(TAG, "Remote error", e);
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
Log.d(TAG, "daemon onServiceDisconnected");
testIPC = null;
Log.d(TAG, "AIDL onServiceDisconnected");
if (isDaemon) daemonConn = null;
else aidlConn = null;
refreshUI();
}
}
private MSGConnection msgConn;
class MSGConnection implements ServiceConnection {
private Messenger m;
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Log.d(TAG, "service onServiceConnected");
remoteMessenger = new Messenger(service);
if (serviceTestQueued) {
serviceTestQueued = false;
testService();
Log.d(TAG, "MSG onServiceConnected");
m = new Messenger(service);
msgConn = this;
refreshUI();
Message msg = Message.obtain(null, MSGService.MSG_GETINFO);
msg.replyTo = me;
try {
m.send(msg);
} catch (RemoteException e) {
Log.e(TAG, "Remote error", e);
} finally {
msg.recycle();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
Log.d(TAG, "service onServiceDisconnected");
remoteMessenger = null;
Log.d(TAG, "MSG onServiceDisconnected");
msgConn = null;
refreshUI();
}
}
private void testDaemon() {
try {
consoleList.add("Daemon PID: " + testIPC.getPid());
consoleList.add("Daemon UID: " + testIPC.getUid());
String[] cmds = testIPC.readCmdline().split(" ");
if (cmds.length > 5) {
cmds = Arrays.copyOf(cmds, 6);
cmds[5] = "...";
void stop() {
if (m == null)
return;
Message msg = Message.obtain(null, MSGService.MSG_STOP);
try {
m.send(msg);
} catch (RemoteException e) {
Log.e(TAG, "Remote error", e);
} finally {
msg.recycle();
}
consoleList.add("/proc/cmdline:");
consoleList.addAll(Arrays.asList(cmds));
} catch (RemoteException e) {
Log.e(TAG, "Remote error", e);
}
}
private void testService() {
Message message = Message.obtain(null, MSGService.MSG_GETINFO);
message.replyTo = myMessenger;
try {
remoteMessenger.send(message);
} catch (RemoteException e) {
Log.e(TAG, "Remote error", e);
}
}
@Override
public boolean handleMessage(@NonNull Message msg) {
consoleList.add("Remote PID: " + msg.arg1);
consoleList.add("Remote UID: " + msg.arg2);
String cmdline = msg.getData().getString(MSGService.CMDLINE_KEY);
String[] cmds = cmdline.split(" ");
if (cmds.length > 5) {
cmds = Arrays.copyOf(cmds, 6);
cmds[5] = "...";
}
consoleList.add("/proc/cmdline:");
consoleList.addAll(Arrays.asList(cmds));
consoleList.add("MSG PID : " + msg.arg1);
consoleList.add("MSG UID : " + msg.arg2);
String uuid = msg.getData().getString(MSGService.UUID_KEY);
consoleList.add("MSG UUID : " + uuid);
return false;
}
private void refreshUI() {
binding.aidlSvc.setText(aidlConn == null ? "Bind AIDL" : "Unbind AIDL");
binding.msgSvc.setText(msgConn == null ? "Bind MSG" : "Unbind MSG");
binding.testDaemon.setText(daemonConn == null ? "Bind Daemon" : "Unbind Daemon");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
// Bind to a root service; IPC via Messages
binding.testSvc.setOnClickListener(v -> {
if (remoteMessenger == null) {
serviceTestQueued = true;
Intent intent = new Intent(this, MSGService.class);
RootService.bind(intent, conn);
return;
}
testService();
});
// Unbind service through RootService API
binding.unbindSvc.setOnClickListener(v -> RootService.unbind(conn));
// Send a message to service and ask it to stop itself to demonstrate stopSelf()
binding.stopSvc.setOnClickListener(v -> {
if (remoteMessenger != null) {
Message message = Message.obtain(null, MSGService.MSG_STOP);
try {
remoteMessenger.send(message);
} catch (RemoteException e) {
Log.e(TAG, "Remote error", e);
}
}
});
// Bind to a daemon root service; IPC via AIDL
binding.testDaemon.setOnClickListener(v -> {
if (testIPC == null) {
daemonTestQueued = true;
// Bind to a root service; IPC via AIDL
binding.aidlSvc.setOnClickListener(v -> {
if (aidlConn == null) {
Intent intent = new Intent(this, AIDLService.class);
RootService.bind(intent, new AIDLConnection());
return;
RootService.bind(intent, new AIDLConnection(false));
} else {
RootService.unbind(aidlConn);
}
testDaemon();
});
// Bind to a root service; IPC via Messages
binding.msgSvc.setOnClickListener(v -> {
if (msgConn == null) {
Intent intent = new Intent(this, MSGService.class);
RootService.bind(intent, new MSGConnection());
} else {
RootService.unbind(msgConn);
}
});
// Send a message to service and ask it to stop itself to test stopSelf()
binding.selfStop.setOnClickListener(v -> {
if (msgConn != null) {
msgConn.stop();
}
});
// To verify whether the daemon actually works, kill the app and try testing the
// daemon again. You should get the same PID as last time (as it was re-using the
// previous daemon process), and in AIDLService, onRebind should be called.
// Note: re-running the app in Android Studio is not the same as kill + relaunch.
// The root process will kill itself when it detects the client APK has updated.
// Bind to a daemon root service
binding.testDaemon.setOnClickListener(v -> {
if (daemonConn == null) {
Intent intent = new Intent(this, AIDLService.class);
intent.addCategory(RootService.CATEGORY_DAEMON_MODE);
RootService.bind(intent, new AIDLConnection(true));
} else {
RootService.unbind(daemonConn);
}
});
// Test the stop API
binding.stopDaemon.setOnClickListener(v -> {
Intent intent = new Intent(this, AIDLService.class);
// Use stop here instead of unbind because AIDLService is running as a daemon.
// To verify whether the daemon actually works, kill the app and try testing the
// daemon again. You should get the same PID as last time (as it was re-using the
// previous daemon process), and in AIDLService, onRebind should be called.
// Note: re-running the app in Android Studio is not the same as kill + relaunch.
// The root process will kill itself when it detects the client APK has updated.
intent.addCategory(RootService.CATEGORY_DAEMON_MODE);
RootService.stop(intent);
});

View File

@ -72,23 +72,23 @@
android:orientation="horizontal">
<Button
android:id="@+id/test_svc"
android:id="@+id/aidl_svc"
style="?android:borderlessButtonStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Test SVC" />
android:text="Bind AIDL" />
<Button
android:id="@+id/unbind_svc"
android:id="@+id/msg_svc"
style="?android:borderlessButtonStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Unbind" />
android:text="Bind MSG" />
<Button
android:id="@+id/stop_svc"
android:id="@+id/self_stop"
style="?android:borderlessButtonStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
@ -118,7 +118,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Test Daemon" />
android:text="Bind Daemon" />
<Button
android:id="@+id/stop_daemon"

View File

@ -5,8 +5,8 @@ package com.topjohnwu.superuser.internal;
interface IRootServiceManager {
oneway void broadcast(int uid, String action);
oneway void stop(in ComponentName name);
oneway void connect(in Bundle bundle);
oneway void stop(in ComponentName name, int uid, String action);
oneway void connect(in IBinder binder, boolean debug);
IBinder bind(in Intent intent);
oneway void unbind(in ComponentName name);
}

Binary file not shown.

View File

@ -0,0 +1,38 @@
/*
* Copyright 2022 John "topjohnwu" Wu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.topjohnwu.superuser.internal;
import android.os.IBinder;
import android.os.RemoteException;
abstract class BinderHolder implements IBinder.DeathRecipient {
private final IBinder binder;
BinderHolder(IBinder b) throws RemoteException {
binder = b;
binder.linkToDeath(this, 0);
}
@Override
public final void binderDied() {
binder.unlinkToDeath(this, 0);
UiThreadHandler.run(this::onBinderDied);
}
protected abstract void onBinderDied();
}

View File

@ -24,7 +24,6 @@ import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.util.Log;
import android.util.Pair;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
@ -44,12 +43,13 @@ Expected command-line args:
args[0]: client service component name
args[1]: client UID
args[2]: client broadcast receiver intent filter
args[3]: {@link #CMDLINE_START_SERVICE} or {@link #CMDLINE_STOP_SERVICE}
args[3]: CMDLINE_START_SERVICE, CMDLINE_START_DAEMON, or CMDLINE_STOP_SERVICE
*/
class RootServerMain extends ContextWrapper implements Callable<Pair<Integer, String>> {
class RootServerMain extends ContextWrapper implements Callable<Object[]> {
static final String CMDLINE_STOP_SERVICE = "stop";
static final String CMDLINE_START_SERVICE = "start";
static final String CMDLINE_START_DAEMON = "daemon";
static final String CMDLINE_STOP_SERVICE = "stop";
static final Method getService;
static final Method addService;
@ -110,10 +110,15 @@ class RootServerMain extends ContextWrapper implements Callable<Pair<Integer, St
private final int uid;
private final String filter;
private final boolean isDaemon;
@Override
public Pair<Integer, String> call() {
return new Pair<>(uid, filter);
public Object[] call() {
Object[] objs = new Object[3];
objs[0] = uid;
objs[1] = filter;
objs[2] = isDaemon;
return objs;
}
public RootServerMain(String[] args) throws Exception {
@ -123,30 +128,38 @@ class RootServerMain extends ContextWrapper implements Callable<Pair<Integer, St
uid = Integer.parseInt(args[1]);
filter = args[2];
String action = args[3];
boolean stop = false;
// Get existing daemon process
Object binder = getService.invoke(null, getServiceName(name.getPackageName()));
IRootServiceManager m = IRootServiceManager.Stub.asInterface((IBinder) binder);
if (action.equals(CMDLINE_STOP_SERVICE)) {
if (m != null) {
try {
m.stop(name);
// If the process wasn't killed yet, send another broadcast
m.broadcast(uid, filter);
} catch (RemoteException ignored) {}
}
System.exit(0);
switch (action) {
case CMDLINE_STOP_SERVICE:
stop = true;
// fallthrough
case CMDLINE_START_DAEMON:
isDaemon = true;
break;
default:
isDaemon = false;
break;
}
if (m != null) {
try {
if (isDaemon) daemon: try {
// Get existing daemon process
Object binder = getService.invoke(null, getServiceName(name.getPackageName()));
IRootServiceManager m = IRootServiceManager.Stub.asInterface((IBinder) binder);
if (m == null)
break daemon;
if (stop) {
m.stop(name, uid, filter);
} else {
m.broadcast(uid, filter);
// Terminate process if broadcast went through
// Terminate process if broadcast went through without exception
System.exit(0);
} catch (RemoteException ignored) {
// Daemon process dead, continue
}
} catch (RemoteException ignored) {
} finally {
if (stop)
System.exit(0);
}
Context systemContext = getSystemContext();

View File

@ -16,8 +16,11 @@
package com.topjohnwu.superuser.internal;
import static com.topjohnwu.superuser.internal.RootServerMain.CMDLINE_START_DAEMON;
import static com.topjohnwu.superuser.internal.RootServerMain.CMDLINE_START_SERVICE;
import static com.topjohnwu.superuser.internal.RootServerMain.CMDLINE_STOP_SERVICE;
import static com.topjohnwu.superuser.internal.Utils.newArrayMap;
import static com.topjohnwu.superuser.ipc.RootService.CATEGORY_DAEMON_MODE;
import android.annotation.SuppressLint;
import android.content.BroadcastReceiver;
@ -36,7 +39,6 @@ import android.os.Message;
import android.os.Messenger;
import android.os.Process;
import android.os.RemoteException;
import android.util.ArrayMap;
import android.util.Pair;
import androidx.annotation.NonNull;
@ -51,7 +53,6 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
@ -60,12 +61,11 @@ import java.util.UUID;
import java.util.concurrent.Executor;
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class RootServiceManager implements IBinder.DeathRecipient, Handler.Callback {
public class RootServiceManager implements Handler.Callback {
private static RootServiceManager mInstance;
static final String TAG = "IPC";
static final String BUNDLE_DEBUG_KEY = "debug";
static final String BUNDLE_BINDER_KEY = "binder";
static final String LOGGING_ENV = "LIBSU_VERBOSE_LOGGING";
@ -74,6 +74,9 @@ public class RootServiceManager implements IBinder.DeathRecipient, Handler.Callb
private static final String INTENT_EXTRA_KEY = "extra.bundle";
private static final int REMOTE_EN_ROUTE = 1 << 0;
private static final int DAEMON_EN_ROUTE = 1 << 1;
public static RootServiceManager getInstance() {
if (mInstance == null) {
mInstance = new RootServiceManager();
@ -98,7 +101,7 @@ public class RootServiceManager implements IBinder.DeathRecipient, Handler.Callb
}
@NonNull
private static ComponentName enforceIntent(Intent intent) {
private static Pair<ComponentName, Boolean> enforceIntent(Intent intent) {
ComponentName name = intent.getComponent();
if (name == null) {
throw new IllegalArgumentException("The intent does not have a component set");
@ -106,38 +109,35 @@ public class RootServiceManager implements IBinder.DeathRecipient, Handler.Callb
if (!name.getPackageName().equals(Utils.getContext().getPackageName())) {
throw new IllegalArgumentException("RootServices outside of the app are not supported");
}
return name;
return new Pair<>(name, intent.hasCategory(CATEGORY_DAEMON_MODE));
}
private IRootServiceManager mRM;
private List<Runnable> pendingTasks;
private static void disconnect(Map.Entry<ServiceConnection, Pair<RemoteService, Executor>> e) {
e.getValue().second.execute(() ->
e.getKey().onServiceDisconnected(e.getValue().first.key.first));
}
private RemoteProcess mRemote;
private RemoteProcess mDaemon;
private String filterAction;
private int flags = 0;
private final Map<ComponentName, RemoteService> services;
private final Map<ServiceConnection, Pair<RemoteService, Executor>> connections;
private final List<BindTask> pendingTasks = new ArrayList<>();
private final Map<Pair<ComponentName, Boolean>, RemoteService> services = newArrayMap();
private final Map<ServiceConnection, Pair<RemoteService, Executor>> connections = newArrayMap();
private RootServiceManager() {
if (Build.VERSION.SDK_INT >= 19) {
services = new ArrayMap<>();
connections = new ArrayMap<>();
} else {
services = new HashMap<>();
connections = new HashMap<>();
}
}
private RootServiceManager() {}
private Runnable createStartRootProcessTask(ComponentName name, String action) {
Context context = Utils.getContext();
Bundle b = null;
boolean[] debug = new boolean[1];
if (filterAction == null) {
filterAction = UUID.randomUUID().toString();
Bundle connectArgs = new Bundle();
b = connectArgs;
// Receive ACK and service stop signal
Handler h = new Handler(Looper.getMainLooper(), this);
Messenger m = new Messenger(h);
connectArgs.putBinder(BUNDLE_BINDER_KEY, m.getBinder());
// Register receiver to receive binder from root process
IntentFilter filter = new IntentFilter(filterAction);
@ -150,9 +150,9 @@ public class RootServiceManager implements IBinder.DeathRecipient, Handler.Callb
IBinder binder = bundle.getBinder(BUNDLE_BINDER_KEY);
if (binder == null)
return;
IRootServiceManager m = IRootServiceManager.Stub.asInterface(binder);
IRootServiceManager sm = IRootServiceManager.Stub.asInterface(binder);
try {
m.connect(connectArgs);
sm.connect(m.getBinder(), debug[0]);
} catch (RemoteException e) {
Utils.err(TAG, e);
}
@ -173,9 +173,7 @@ public class RootServiceManager implements IBinder.DeathRecipient, Handler.Callb
// Only support debugging on SDK >= 27
if (Build.VERSION.SDK_INT >= 27 && Debug.isDebuggerConnected()) {
if (b != null) {
b.putBoolean(BUNDLE_DEBUG_KEY, true);
}
debug[0] = true;
// Reference of the params to start jdwp:
// https://developer.android.com/ndk/guides/wrap-script#debugging_when_using_wrapsh
if (Build.VERSION.SDK_INT == 27) {
@ -208,51 +206,53 @@ public class RootServiceManager implements IBinder.DeathRecipient, Handler.Callb
};
}
private boolean bind(Intent intent, Executor executor, ServiceConnection conn) {
// Returns null if binding is done synchronously, or else return key
private Pair<ComponentName, Boolean> bindInternal(
Intent intent, Executor executor, ServiceConnection conn) {
enforceMainThread();
// Local cache
ComponentName name = enforceIntent(intent);
RemoteService s = services.get(name);
Pair<ComponentName, Boolean> key = enforceIntent(intent);
RemoteService s = services.get(key);
if (s != null) {
connections.put(conn, new Pair<>(s, executor));
s.refCount++;
executor.execute(() -> conn.onServiceConnected(name, s.binder));
return true;
executor.execute(() -> conn.onServiceConnected(key.first, s.binder));
return null;
}
if (mRM == null)
return false;
RemoteProcess p = key.second ? mDaemon : mRemote;
if (p == null)
return key;
try {
IBinder binder = mRM.bind(intent);
IBinder binder = p.sm.bind(intent);
if (binder != null) {
RemoteService r = new RemoteService(name, binder);
RemoteService r = new RemoteService(key, binder, p);
connections.put(conn, new Pair<>(r, executor));
services.put(name, r);
executor.execute(() -> conn.onServiceConnected(name, binder));
services.put(key, r);
executor.execute(() -> conn.onServiceConnected(key.first, binder));
} else if (Build.VERSION.SDK_INT >= 28) {
executor.execute(() -> conn.onNullBinding(name));
executor.execute(() -> conn.onNullBinding(key.first));
}
} catch (RemoteException e) {
Utils.err(TAG, e);
mRM = null;
return false;
p.binderDied();
return key;
}
return true;
return null;
}
public Runnable createBindTask(Intent intent, Executor executor, ServiceConnection conn) {
if (!bind(intent, executor, conn)) {
boolean launch = false;
if (pendingTasks == null) {
pendingTasks = new ArrayList<>();
launch = true;
}
pendingTasks.add(() -> bind(intent, executor, conn));
if (launch) {
return createStartRootProcessTask(intent.getComponent(), CMDLINE_START_SERVICE);
Pair<ComponentName, Boolean> key = bindInternal(intent, executor, conn);
if (key != null) {
pendingTasks.add(() -> bindInternal(intent, executor, conn) == null);
int mask = key.second ? DAEMON_EN_ROUTE : REMOTE_EN_ROUTE;
String action = key.second ? CMDLINE_START_DAEMON : CMDLINE_START_SERVICE;
if ((flags & mask) == 0) {
flags |= mask;
return createStartRootProcessTask(key.first, action);
}
}
return null;
@ -261,18 +261,15 @@ public class RootServiceManager implements IBinder.DeathRecipient, Handler.Callb
public void unbind(@NonNull ServiceConnection conn) {
enforceMainThread();
if (mRM == null)
return;
Pair<RemoteService, Executor> p = connections.remove(conn);
if (p != null) {
p.first.refCount--;
p.second.execute(() -> conn.onServiceDisconnected(p.first.name));
p.second.execute(() -> conn.onServiceDisconnected(p.first.key.first));
if (p.first.refCount == 0) {
// Actually close the service
services.remove(p.first.name);
services.remove(p.first.key);
try {
mRM.unbind(p.first.name);
p.first.host.sm.unbind(p.first.key.first);
} catch (RemoteException e) {
Utils.err(TAG, e);
}
@ -280,10 +277,10 @@ public class RootServiceManager implements IBinder.DeathRecipient, Handler.Callb
}
}
private boolean stopInternal(ComponentName name) {
RemoteService s = services.remove(name);
private void stopInternal(Pair<ComponentName, Boolean> key) {
RemoteService s = services.remove(key);
if (s == null)
return false;
return;
// Notify all connections
Iterator<Map.Entry<ServiceConnection, Pair<RemoteService, Executor>>> it =
@ -291,52 +288,34 @@ public class RootServiceManager implements IBinder.DeathRecipient, Handler.Callb
while (it.hasNext()) {
Map.Entry<ServiceConnection, Pair<RemoteService, Executor>> e = it.next();
if (e.getValue().first.equals(s)) {
e.getValue().second.execute(() -> e.getKey().onServiceDisconnected(name));
disconnect(e);
it.remove();
}
}
return true;
}
public void stop(Intent intent) {
enforceMainThread();
ComponentName name = enforceIntent(intent);
if (mRM == null) {
// Start a new root process
Runnable r = createStartRootProcessTask(name, CMDLINE_STOP_SERVICE);
Shell.EXECUTOR.execute(r);
Pair<ComponentName, Boolean> key = enforceIntent(intent);
RemoteProcess p = key.second ? mDaemon : mRemote;
if (p == null) {
if (key.second) {
// Start a new root process to stop daemon
Runnable r = createStartRootProcessTask(key.first, CMDLINE_STOP_SERVICE);
Shell.EXECUTOR.execute(r);
}
return;
}
if (!stopInternal(name))
return;
stopInternal(key);
try {
mRM.stop(name);
p.sm.stop(key.first, -1, null);
} catch (RemoteException e) {
Utils.err(TAG, e);
}
}
@Override
public void binderDied() {
UiThreadHandler.run(() -> {
if (mRM != null) {
mRM.asBinder().unlinkToDeath(this, 0);
mRM = null;
}
// Notify all connections
for (Map.Entry<ServiceConnection, Pair<RemoteService, Executor>> e
: connections.entrySet()) {
e.getValue().second.execute(() ->
e.getKey().onServiceDisconnected(e.getValue().first.name));
}
connections.clear();
services.clear();
});
}
@Override
public boolean handleMessage(@NonNull Message msg) {
switch (msg.what) {
@ -344,35 +323,81 @@ public class RootServiceManager implements IBinder.DeathRecipient, Handler.Callb
IBinder b = ((Bundle) msg.obj).getBinder(BUNDLE_BINDER_KEY);
if (b == null)
return false;
RemoteProcess p;
try {
b.linkToDeath(this, 0);
p = new RemoteProcess(b);
} catch (RemoteException e) {
return false;
}
mRM = IRootServiceManager.Stub.asInterface(b);
List<Runnable> tasks = pendingTasks;
pendingTasks = null;
if (tasks != null) {
for (Runnable r : tasks) {
r.run();
if (msg.arg1 == 0) {
mRemote = p;
flags &= ~REMOTE_EN_ROUTE;
} else {
mDaemon = p;
flags &= ~DAEMON_EN_ROUTE;
}
for (int i = pendingTasks.size() - 1; i >= 0; --i) {
if (pendingTasks.get(i).run()) {
pendingTasks.remove(i);
}
}
break;
case MSG_STOP:
stopInternal((ComponentName) msg.obj);
stopInternal(new Pair<>((ComponentName) msg.obj, msg.arg1 != 0));
break;
}
return false;
}
static class RemoteService {
final ComponentName name;
final IBinder binder;
int refCount = 1;
class RemoteProcess extends BinderHolder {
RemoteService(ComponentName name, IBinder binder) {
this.name = name;
this.binder = binder;
final IRootServiceManager sm;
RemoteProcess(IBinder b) throws RemoteException {
super(b);
sm = IRootServiceManager.Stub.asInterface(b);
}
@Override
protected void onBinderDied() {
if (mRemote == this)
mRemote = null;
if (mDaemon == this)
mDaemon = null;
Iterator<RemoteService> sit = services.values().iterator();
while (sit.hasNext()) {
if (sit.next().host == this) {
sit.remove();
}
}
Iterator<Map.Entry<ServiceConnection, Pair<RemoteService, Executor>>> it =
connections.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<ServiceConnection, Pair<RemoteService, Executor>> e = it.next();
if (e.getValue().first.host == this) {
disconnect(e);
it.remove();
}
}
}
}
static class RemoteService {
final Pair<ComponentName, Boolean> key;
final IBinder binder;
final RemoteProcess host;
int refCount = 1;
RemoteService(Pair<ComponentName, Boolean> key, IBinder binder, RemoteProcess host) {
this.key = key;
this.binder = binder;
this.host = host;
}
}
interface BindTask {
boolean run();
}
}

View File

@ -19,12 +19,13 @@ package com.topjohnwu.superuser.internal;
import static com.topjohnwu.superuser.internal.RootServerMain.attachBaseContext;
import static com.topjohnwu.superuser.internal.RootServerMain.getServiceName;
import static com.topjohnwu.superuser.internal.RootServiceManager.BUNDLE_BINDER_KEY;
import static com.topjohnwu.superuser.internal.RootServiceManager.BUNDLE_DEBUG_KEY;
import static com.topjohnwu.superuser.internal.RootServiceManager.LOGGING_ENV;
import static com.topjohnwu.superuser.internal.RootServiceManager.MSG_ACK;
import static com.topjohnwu.superuser.internal.RootServiceManager.MSG_STOP;
import static com.topjohnwu.superuser.internal.RootServiceManager.TAG;
import static com.topjohnwu.superuser.internal.Utils.context;
import static com.topjohnwu.superuser.internal.Utils.newArrayMap;
import static com.topjohnwu.superuser.internal.Utils.newArraySet;
import android.annotation.SuppressLint;
import android.content.ComponentName;
@ -39,8 +40,7 @@ import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.ArrayMap;
import android.util.Pair;
import android.util.SparseArray;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
@ -50,66 +50,66 @@ import com.topjohnwu.superuser.ipc.RootService;
import java.io.File;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class RootServiceServer extends IRootServiceManager.Stub implements IBinder.DeathRecipient {
public class RootServiceServer extends IRootServiceManager.Stub {
private static RootServiceServer mInstance;
@SuppressWarnings("rawtypes")
public static RootServiceServer getInstance(Context context) {
if (mInstance == null) {
mInstance = new RootServiceServer(context);
if (context instanceof Callable) {
try {
Pair p = (Pair) ((Callable) context).call();
mInstance.broadcast((int) p.first, (String) p.second);
} catch (Exception ignored) {}
}
}
return mInstance;
}
@SuppressWarnings("FieldCanBeLocal")
private final FileObserver observer; /* A strong reference is required */
private final Map<ComponentName, ServiceContainer> activeServices;
private Messenger client;
private boolean isDaemon = false;
private final Map<ComponentName, ServiceContainer> activeServices = newArrayMap();
private final SparseArray<ClientProcess> clients = new SparseArray<>();
private final boolean isDaemon;
@SuppressWarnings("rawtypes")
private RootServiceServer(Context context) {
Shell.enableVerboseLogging = System.getenv(LOGGING_ENV) != null;
Utils.context = Utils.getContextImpl(context);
if (Build.VERSION.SDK_INT >= 19) {
activeServices = new ArrayMap<>();
} else {
activeServices = new HashMap<>();
}
observer = new AppObserver(new File(context.getPackageCodePath()));
observer.startWatching();
if (context instanceof Callable) {
try {
Object[] objs = (Object[]) ((Callable) context).call();
broadcast((int) objs[0], (String) objs[1]);
isDaemon = (boolean) objs[2];
if (isDaemon) {
// Register ourselves as system service
HiddenAPIs.addService(getServiceName(context.getPackageName()), this);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
} else {
throw new IllegalArgumentException("Expected Context to be Callable");
}
}
@Override
public void connect(Bundle bundle) {
if (client != null)
public void connect(IBinder binder, boolean debug) {
ClientProcess c = clients.get(getCallingUid());
if (c != null)
return;
IBinder binder = bundle.getBinder(BUNDLE_BINDER_KEY);
if (binder == null)
return;
final Messenger c;
try {
binder.linkToDeath(this, 0);
c = new Messenger(binder);
c = new ClientProcess(binder);
} catch (RemoteException e) {
Utils.err(TAG, e);
return;
}
if (bundle.getBoolean(BUNDLE_DEBUG_KEY, false)) {
if (debug) {
// ActivityThread.attach(true, 0) will set this to system_process
HiddenAPIs.setAppName(context.getPackageName() + ":root");
Utils.log(TAG, "Waiting for debugger to be attached...");
@ -121,15 +121,18 @@ public class RootServiceServer extends IRootServiceManager.Stub implements IBind
Utils.log(TAG, "Debugger attached!");
}
Bundle bundle = new Bundle();
bundle.putBinder(BUNDLE_BINDER_KEY, this);
Message m = Message.obtain();
m.what = MSG_ACK;
bundle = new Bundle();
bundle.putBinder(BUNDLE_BINDER_KEY, this);
m.arg1 = isDaemon ? 1 : 0;
m.obj = bundle;
try {
c.send(m);
client = c;
} catch (RemoteException ignored) {
c.m.send(m);
clients.put(c.uid, c);
} catch (RemoteException e) {
Utils.err(TAG, e);
} finally {
m.recycle();
}
@ -137,6 +140,9 @@ public class RootServiceServer extends IRootServiceManager.Stub implements IBind
@SuppressLint("MissingPermission")
public void broadcast(int uid, String action) {
// Use the UID argument iff caller is root
uid = getCallingUid() == 0 ? uid : getCallingUid();
Utils.log(TAG, "broadcast to uid=" + uid);
Intent intent = RootServiceManager.getBroadcastIntent(context, action, this);
if (Build.VERSION.SDK_INT >= 24) {
UserHandle h = UserHandle.getUserHandleForUid(uid);
@ -149,9 +155,10 @@ public class RootServiceServer extends IRootServiceManager.Stub implements IBind
@Override
public IBinder bind(Intent intent) {
IBinder[] b = new IBinder[1];
int uid = getCallingUid();
UiThreadHandler.runAndWait(() -> {
try {
b[0] = bindInternal(intent);
b[0] = bindInternal(uid, intent);
} catch (Exception e) {
Utils.err(TAG, e);
}
@ -161,47 +168,55 @@ public class RootServiceServer extends IRootServiceManager.Stub implements IBind
@Override
public void unbind(ComponentName name) {
int uid = getCallingUid();
UiThreadHandler.run(() -> {
Utils.log(TAG, name.getClassName() + " unbind");
stopService(name, false);
stopService(uid, name, false);
});
}
@Override
public void stop(ComponentName name) {
public void stop(ComponentName name, int uid, String action) {
// Use the UID argument iff caller is root
int clientUid = getCallingUid() == 0 ? uid : getCallingUid();
UiThreadHandler.run(() -> {
Utils.log(TAG, name.getClassName() + " stop");
stopService(name, true);
});
}
@Override
public void binderDied() {
Messenger c = client;
client = null;
if (c != null)
c.getBinder().unlinkToDeath(this, 0);
UiThreadHandler.run(() -> {
Utils.log(TAG, "Client process terminated");
stopAllService(false);
stopService(clientUid, name, true);
if (action != null) {
// If we aren't killed yet, send another broadcast
broadcast(clientUid, action);
}
});
}
public void selfStop(ComponentName name) {
UiThreadHandler.run(() -> {
ServiceContainer s = activeServices.get(name);
if (s == null)
return;
Utils.log(TAG, name.getClassName() + " selfStop");
stopService(name, true);
Messenger c = client;
if (c != null) {
Message m = Message.obtain();
m.what = MSG_STOP;
m.obj = name;
// Backup all users as we need to notify them
Integer[] users = s.users.toArray(new Integer[0]);
s.users.clear();
stopService(-1, name, true);
// Notify all users
for (int uid : users) {
ClientProcess c = clients.get(uid);
if (c == null)
continue;
Message msg = Message.obtain();
msg.what = MSG_STOP;
msg.arg1 = isDaemon ? 1 : 0;
msg.obj = name;
try {
c.send(m);
c.m.send(msg);
} catch (RemoteException e) {
Utils.err(TAG, e);
} finally {
m.recycle();
msg.recycle();
}
}
});
@ -213,51 +228,51 @@ public class RootServiceServer extends IRootServiceManager.Stub implements IBind
activeServices.put(service.getComponentName(), c);
}
private IBinder bindInternal(Intent intent) throws Exception {
private IBinder bindInternal(int uid, Intent intent) throws Exception {
ClientProcess c = clients.get(uid);
if (c == null)
return null;
ComponentName name = intent.getComponent();
ServiceContainer c = activeServices.get(name);
if (c == null) {
ServiceContainer s = activeServices.get(name);
if (s == null) {
Class<?> clz = Class.forName(name.getClassName());
Constructor<?> ctor = clz.getDeclaredConstructor();
ctor.setAccessible(true);
attachBaseContext.invoke(ctor.newInstance(), context);
// RootService should be registered after attachBaseContext
c = activeServices.get(name);
if (c == null) {
s = activeServices.get(name);
if (s == null) {
return null;
}
}
if (c.binder != null) {
if (s.binder != null) {
Utils.log(TAG, name.getClassName() + " rebind");
c.service.onRebind(c.intent);
if (s.rebind)
s.service.onRebind(s.intent);
} else {
Utils.log(TAG, name.getClassName() + " bind");
c.binder = c.service.onBind(intent);
c.intent = intent.cloneFilter();
s.binder = s.service.onBind(intent);
s.intent = intent.cloneFilter();
}
s.users.add(uid);
return c.binder;
return s.binder;
}
private void setAsDaemon() {
if (!isDaemon) {
// Register ourselves as system service
HiddenAPIs.addService(getServiceName(context.getPackageName()), this);
isDaemon = true;
}
}
private void stopService(ComponentName className, boolean force) {
ServiceContainer c = activeServices.get(className);
if (c != null) {
if (!c.service.onUnbind(c.intent) || force) {
c.service.onDestroy();
activeServices.remove(className);
} else {
setAsDaemon();
private void stopService(int uid, ComponentName name, boolean destroy) {
ServiceContainer s = activeServices.get(name);
if (s != null) {
s.users.remove(uid);
if (s.users.isEmpty()) {
s.rebind = s.service.onUnbind(s.intent);
if (destroy || !isDaemon) {
s.service.onDestroy();
activeServices.remove(name);
}
}
}
if (activeServices.isEmpty()) {
@ -266,19 +281,25 @@ public class RootServiceServer extends IRootServiceManager.Stub implements IBind
}
}
private void stopAllService(boolean force) {
private void stopServices(int uid) {
Iterator<Map.Entry<ComponentName, ServiceContainer>> it =
activeServices.entrySet().iterator();
while (it.hasNext()) {
ServiceContainer c = it.next().getValue();
if (!c.service.onUnbind(c.intent) || force) {
c.service.onDestroy();
it.remove();
} else {
setAsDaemon();
ServiceContainer s = it.next().getValue();
if (uid < 0)
s.users.clear();
else
s.users.remove(uid);
if (s.users.isEmpty()) {
s.rebind = s.service.onUnbind(s.intent);
if (uid < 0 || !isDaemon) {
s.service.onDestroy();
it.remove();
}
}
}
if (force || activeServices.isEmpty()) {
if (activeServices.isEmpty()) {
// Terminate root process
System.exit(0);
}
@ -300,15 +321,37 @@ public class RootServiceServer extends IRootServiceManager.Stub implements IBind
if (event == DELETE_SELF || name.equals(path)) {
UiThreadHandler.run(() -> {
Utils.log(TAG, "App updated, terminate");
stopAllService(true);
stopServices(-1);
System.exit(0);
});
}
}
}
class ClientProcess extends BinderHolder {
final Messenger m;
final int uid;
ClientProcess(IBinder b) throws RemoteException {
super(b);
m = new Messenger(b);
uid = getCallingUid();
}
@Override
protected void onBinderDied() {
Utils.log(TAG, "Client process terminated");
clients.remove(uid);
stopServices(uid);
}
}
static class ServiceContainer {
RootService service;
Intent intent;
IBinder binder;
boolean rebind;
final Set<Integer> users = newArraySet();
}
}

View File

@ -80,6 +80,16 @@ import java.util.concurrent.Executor;
*/
public abstract class RootService extends ContextWrapper {
/**
* Launch the service in "Daemon Mode".
* <p>
* Add this category in the intents passed to {@link #bind(Intent, ServiceConnection)},
* {@link #bind(Intent, Executor, ServiceConnection)}, or
* {@link #createBindTask(Intent, Executor, ServiceConnection)}
* to have the service launched in "Daemon Mode".
*/
public static final String CATEGORY_DAEMON_MODE = "com.topjohnwu.superuser.DAEMON_MODE";
/**
* Connect to a root service, creating it if needed.
* @param intent identifies the service to connect to.