libsu

repository·master·Indexed 24 days ago

https://github.com/topjohnwu/libsu

An Android library for apps requiring root permissions. It provides a 'core' module for Unix shell interaction via Shell.cmd(), a 'service' module for running Java/Kotlin or C/C++ code in a dedicated root process via IPC, and an 'nio' module for remote file system support.

Tokens
2.9K
Snippets
6
Records
7
Agent score
35%

What's inside libsu

  1. How the main shell works and how to configure it

    master

    libsu uses a concept called the "main shell". For each process, there is a single globally shared "main shell" that is constructed on-demand and cached.

    Important: You must set default configurations using Shell.setDefaultBuilder(...) before the main Shell instance is created (e.g., in a static block in your Application or Activity class).

    To avoid blocking the application flow while waiting for root permission prompts, you can preload the main shell using Shell.getShell(callback) during a splash screen.

    static {
        // Set settings before the main shell can be created
        Shell.enableVerboseLogging = BuildConfig.DEBUG;
        Shell.setDefaultBuilder(Shell.Builder.create()
            .setFlags(Shell.FLAG_MOUNT_MASTER)
            .setInitializers(ShellInit.class)
            .setTimeout(10));
    }
    
    // Preload the shell
    Shell.getShell(shell -> {
        // The main shell is now constructed and cached
        exitSplashScreen();
    });
  2. Implement and bind a RootService

    master

    A RootService is a service that runs in a separate root process. It uses Android's Binder IPC to communicate with your main application process. This allows you to run complex Java/Kotlin or C/C++ (via JNI) code with root permissions.

    To use this, you must include com.github.topjohnwu.libsu:service as a dependency.

    Implementation Steps:

    1. Extend RootService and override onBind(Intent intent) to return an IBinder (from a Messenger or AIDL stub).
    2. Create a ServiceConnection (e.g., RootConnection).
    3. Bind to the service using RootService.bind(intent, connection).
    public class RootConnection implements ServiceConnection { ... }
    
    public class ExampleService extends RootService {
        @Override
        public IBinder onBind(Intent intent) {
            // Return IBinder from Messenger or AIDL stub implementation
            return ...;
        }
    }
    
    // Binding from the client
    RootConnection connection = new RootConnection();
    Intent intent = new Intent(context, ExampleService.class);
    RootService.bind(intent, connection);
  3. Install libsu via Gradle

    master

    To use libsu, add the JitPack repository and include the desired modules in your build.gradle file. Note that the library requires Java 8 compatibility.

    Modules available:

    • core: Provides APIs to interact with a Unix (root) shell.
    • service: (Optional) Provides APIs for creating and managing root services via IPC.
    • nio: (Optional) Provides remote file system support via the service module.
    android {
        compileOptions {
            // The library uses Java 8 features
            sourceCompatibility JavaVersion.VERSION_1_8
            targetCompatibility JavaVersion.VERSION_1_8
        }
    }
    repositories {
        maven { url 'https://jitpack.io' }
    }
    dependencies {
        def libsuVersion = '6.0.0'
    
        // The core module that provides APIs to a shell
        implementation "com.github.topjohnwu.libsu:core:${libsuVersion}"
    
        // Optional: APIs for creating root services. Depends on ":core"
        implementation "com.github.topjohnwu.libsu:service:${libsuVersion}"
    
        // Optional: Provides remote file system support
        implementation "com.github.topjohnwu.libsu:nio:${libsuVersion}"
    }
  4. Debug Root Services

    master

    If the application process creating the root service has a debugger attached, the root service will automatically enable debugging mode and wait for a debugger to attach.

    To attach in Android Studio:

    1. Go to "Run > Attach Debugger to Android Process".
    2. Tick the "Show all processes" box.
    3. Manually attach to the remote root process.

    Note: Currently, only the "Java only" debugger is supported.

  5. Initialize shells with Shell.Initializer

    master

    You can customize shell initialization (similar to .bashrc) by providing a Shell.Initializer. This is configured via the Shell.Builder before the main shell is created.

    In onInit(Context context, Shell shell), you can use the provided Shell instance to run a sequence of commands or load scripts to set up the environment (e.g., setting environment variables).

    public class ExampleInitializer extends Shell.Initializer {
        @Override
        public boolean onInit(Context context, Shell shell) {
            InputStream bashrc = context.getResources().openRawResource(R.raw.bashrc);
            shell.newJob()
                .add(bashrc)                  /* Load a script */
                .add("export ENV_VAR=VALUE")  /* Run some commands */
                .exec();
            return true;  // Return false if initialization failed
        }
    }
    
    // Apply to builder
    Shell.Builder builder = Shell.Builder.create();
    builder.setInitializers(ExampleInitializer.class);
  6. Execute shell commands with Shell.cmd()

    master

    You can perform shell operations using static Shell.cmd(...) methods. These methods interact with the main root shell.

    Synchronous Execution

    Use .exec() to run commands and wait for the result.

    Asynchronous Execution

    • .submit(): Fire and forget.
    • .submit(callback): Submit and receive the result via a callback.
    • .enqueue(): Returns a Future<Shell.Result>.

    Handling Output

    • .to(List<String>): Redirect stdout to a list.
    • .to(List<String>, List<String>): Redirect stdout and stderr to separate lists.
    • .to(CallbackList): Receive output in real-time via a callback.

    Loading Scripts

    Instead of executing a script file via sh script.sh, you can load a script from an InputStream (e.g., from res/raw), which behaves similarly to sourcing a script (. script.sh).

    // Execute commands synchronously
    Shell.Result result = Shell.cmd("find /dev/block -iname boot").exec();
    
    List<String> out = result.getOut();  // stdout
    int code = result.getCode();         // return code
    boolean ok = result.isSuccess();     // return code == 0?
    
    // Async APIs
    Shell.cmd("setenforce 0").submit();
    Shell.cmd("sleep 5", "echo hello").submit(result -> updateUI(result));
    Future<Shell.Result> futureResult = Shell.cmd("sleep 5", "echo hello").enqueue();
    
    // Run commands and output to specific Lists
    List<String> mmaps = new ArrayList<>();
    Shell.cmd("cat /proc/1/maps").to(mmaps).exec();
    
    // Receive output in real-time
    List<String> callbackList = new CallbackList<String>() {
        @Override
        public void onAddElement(String s) { updateUI(s); }
    };
    Shell.cmd("for i in $(seq 5); do echo $i; sleep 1; done")
        .to(callbackList)
        .submit(result -> updateUI(result));
  7. Use remote file system APIs with NIO

    master

    The nio module allows you to interact with the file system in the root process from your client process. This requires the com.github.topjohnwu.libsu:nio dependency and a RootService that provides the FileSystemManager service.

    Workflow:

    1. In the Root Service: Return FileSystemManager.getService() in onBind.
    2. In the Client Process: Use the IBinder from your RootService connection to get a FileSystemManager instance via FileSystemManager.getRemote(binder).
    3. Access Files: Use remoteFS.getFile(path) to get an ExtendedFile, which supports standard I/O operations like exists(), newInputStream(), and newOutputStream().
    // --- In the Root Service ---
    public class ExampleService extends RootService {
        @Override
        public IBinder onBind(Intent intent) {
            return FileSystemManager.getService();
        }
    }
    
    // --- In the Client Process ---
    IBinder binder = /* From the root service connection */;
    FileSystemManager remoteFS;
    try {
        remoteFS = FileSystemManager.getRemote(binder);
    } catch (RemoteException e) {
        // Handle errors
    }
    
    ExtendedFile bootBlock = remoteFS.getFile("/dev/block/by-name/boot");
    if (bootBlock.exists()) {
        ExtendedFile bootBackup = remoteFS.getFile("/data/boot.img");
        try (InputStream in = bootBlock.newInputStream();
             OutputStream out = bootBackup.newOutputStream()) {
            // Do I/O stuffs...
        } catch (IOException e) {
            // Handle errors
        }
    }