├── .gitattributes ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build.gradle.kts ├── core ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── topjohnwu │ └── superuser │ ├── CallbackList.java │ ├── NoShellException.java │ ├── Shell.java │ ├── ShellUtils.java │ └── internal │ ├── BuilderImpl.java │ ├── JobTask.java │ ├── MainShell.java │ ├── PendingJob.java │ ├── ResultFuture.java │ ├── ResultHolder.java │ ├── ResultImpl.java │ ├── ShellImpl.java │ ├── ShellInputSource.java │ ├── ShellJob.java │ ├── StreamGobbler.java │ ├── UiThreadHandler.java │ ├── Utils.java │ └── WaitRunnable.java ├── example ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── aidl │ └── com │ │ └── topjohnwu │ │ └── libsuexample │ │ └── ITestService.aidl │ ├── cpp │ ├── CMakeLists.txt │ └── test.cpp │ ├── java │ └── com │ │ └── topjohnwu │ │ └── libsuexample │ │ ├── AIDLService.java │ │ ├── MSGService.java │ │ ├── MainActivity.java │ │ └── StressTest.java │ └── res │ ├── layout │ └── activity_main.xml │ └── raw │ └── bashrc.sh ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── io ├── .gitignore ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── topjohnwu │ └── superuser │ ├── internal │ ├── ByteOutputStream.java │ ├── DataInputImpl.java │ ├── DataOutputImpl.java │ ├── IOFactory.java │ ├── RAFWrapper.java │ ├── ShellBlockIO.java │ ├── ShellIO.java │ └── ShellPipeStream.java │ └── io │ ├── SuFile.java │ ├── SuFileInputStream.java │ ├── SuFileOutputStream.java │ ├── SuRandomAccessFile.java │ └── package-info.java ├── jitpack.yml ├── nio ├── .gitignore ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ ├── aidl │ └── com │ │ └── topjohnwu │ │ └── superuser │ │ └── internal │ │ └── IFileSystemService.aidl │ └── java │ └── com │ └── topjohnwu │ └── superuser │ ├── internal │ ├── FileContainer.java │ ├── FileImpl.java │ ├── FileSystemService.java │ ├── FileUtils.java │ ├── IOResult.java │ ├── LocalFile.java │ ├── NIOFactory.java │ ├── OpenFile.java │ ├── RemoteFile.java │ └── RemoteFileChannel.java │ └── nio │ ├── ExtendedFile.java │ └── FileSystemManager.java ├── service ├── .gitignore ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ ├── aidl │ └── com │ │ └── topjohnwu │ │ └── superuser │ │ └── internal │ │ └── IRootServiceManager.aidl │ ├── assets │ └── main.jar │ └── java │ └── com │ └── topjohnwu │ └── superuser │ ├── internal │ ├── BinderHolder.java │ ├── HiddenAPIs.java │ ├── RootServerMain.java │ ├── RootServiceManager.java │ └── RootServiceServer.java │ └── ipc │ └── RootService.java └── settings.gradle.kts /.gitattributes: -------------------------------------------------------------------------------- 1 | # Make sure scripts are not checked out as CRLF 2 | *.sh text eol=lf 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea 3 | .gradle 4 | /local.properties 5 | /.idea/workspace.xml 6 | /.idea/libraries 7 | .DS_Store 8 | /build 9 | /captures 10 | .externalNativeBuild 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # libsu 2 | 3 | [![](https://jitpack.io/v/topjohnwu/libsu.svg)](https://jitpack.io/#topjohnwu/libsu) 4 | 5 | An Android library providing a complete solution for apps using root permissions. 6 | 7 | `libsu` comes with 2 main components: the `core` module handles the creation of the Unix (root) shell process and wraps it with high level, robust Java APIs; the `service` module handles the launching, binding, and management of root services over IPC, allowing you to run Java/Kotlin and C/C++ code (via JNI) with root permissions. 8 | 9 | ## [Changelog](./CHANGELOG.md) 10 | 11 | ## [Javadoc](https://topjohnwu.github.io/libsu/) 12 | 13 | ## Download 14 | 15 | ```groovy 16 | android { 17 | compileOptions { 18 | // The library uses Java 8 features 19 | sourceCompatibility JavaVersion.VERSION_1_8 20 | targetCompatibility JavaVersion.VERSION_1_8 21 | } 22 | } 23 | repositories { 24 | maven { url 'https://jitpack.io' } 25 | } 26 | dependencies { 27 | def libsuVersion = '6.0.0' 28 | 29 | // The core module that provides APIs to a shell 30 | implementation "com.github.topjohnwu.libsu:core:${libsuVersion}" 31 | 32 | // Optional: APIs for creating root services. Depends on ":core" 33 | implementation "com.github.topjohnwu.libsu:service:${libsuVersion}" 34 | 35 | // Optional: Provides remote file system support 36 | implementation "com.github.topjohnwu.libsu:nio:${libsuVersion}" 37 | } 38 | ``` 39 | 40 | ## Quick Tutorial 41 | 42 | Please note that this is a quick demo going through the key features of `libsu`. Please read the full Javadoc and check out the example app (`:example`) in this project for more details. 43 | 44 | ### Configuration 45 | 46 | Similar to threads where there is a special "main thread", `libsu` also has the concept of the "main shell". For each process, there is a single globally shared "main shell" that is constructed on-demand and cached. Set default configurations before the main `Shell` instance is created: 47 | 48 | ```java 49 | public class SplashActivity extends Activity { 50 | 51 | static { 52 | // Set settings before the main shell can be created 53 | Shell.enableVerboseLogging = BuildConfig.DEBUG; 54 | Shell.setDefaultBuilder(Shell.Builder.create() 55 | .setFlags(Shell.FLAG_MOUNT_MASTER) 56 | .setInitializers(ShellInit.class) 57 | .setTimeout(10)); 58 | } 59 | 60 | @Override 61 | protected void onCreate(Bundle savedInstanceState) { 62 | super.onCreate(savedInstanceState); 63 | showSplashScreen(); 64 | // As an example, preload the main root shell in the splash screen 65 | // so the app can use it afterwards without interrupting application 66 | // flow (e.g. waiting for root permission prompt) 67 | Shell.getShell(shell -> { 68 | // The main shell is now constructed and cached 69 | exitSplashScreen(); 70 | }); 71 | } 72 | } 73 | 74 | ``` 75 | 76 | ### Shell Operations 77 | 78 | `Shell` operations can be performed through static `Shell.cmd(...)` methods that directly use the main root shell: 79 | 80 | ```java 81 | Shell.Result result; 82 | // Execute commands synchronously 83 | result = Shell.cmd("find /dev/block -iname boot").exec(); 84 | // Aside from commands, you can also load scripts from InputStream. 85 | // This is NOT like executing a script like "sh script.sh", but rather 86 | // more similar to sourcing the script (". script.sh"). 87 | result = Shell.cmd(getResources().openRawResource(R.raw.script)).exec(); 88 | 89 | List out = result.getOut(); // stdout 90 | int code = result.getCode(); // return code of the last command 91 | boolean ok = result.isSuccess(); // return code == 0? 92 | 93 | // Async APIs 94 | Shell.cmd("setenforce 0").submit(); // submit and don't care results 95 | Shell.cmd("sleep 5", "echo hello").submit(result -> updateUI(result)); 96 | Future futureResult = Shell.cmd("sleep 5", "echo hello").enqueue(); 97 | 98 | // Run commands and output to specific Lists 99 | List mmaps = new ArrayList<>(); 100 | Shell.cmd("cat /proc/1/maps").to(mmaps).exec(); 101 | List stdout = new ArrayList<>(); 102 | List stderr = new ArrayList<>(); 103 | Shell.cmd("echo hello", "echo hello >&2").to(stdout, stderr).exec(); 104 | 105 | // Receive output in real-time 106 | List callbackList = new CallbackList() { 107 | @Override 108 | public void onAddElement(String s) { updateUI(s); } 109 | }; 110 | Shell.cmd("for i in $(seq 5); do echo $i; sleep 1; done") 111 | .to(callbackList) 112 | .submit(result -> updateUI(result)); 113 | ``` 114 | 115 | ### Initialization 116 | 117 | Optionally, a similar concept to `.bashrc`, initialize shells with custom `Shell.Initializer`: 118 | 119 | ```java 120 | public class ExampleInitializer extends Shell.Initializer { 121 | @Override 122 | public boolean onInit(Context context, Shell shell) { 123 | InputStream bashrc = context.getResources().openRawResource(R.raw.bashrc); 124 | // Here we use Shell instance APIs instead of Shell.cmd(...) static methods 125 | shell.newJob() 126 | .add(bashrc) /* Load a script */ 127 | .add("export ENV_VAR=VALUE") /* Run some commands */ 128 | .exec(); 129 | return true; // Return false to indicate initialization failed 130 | } 131 | } 132 | Shell.Builder builder = /* Create a shell builder */ ; 133 | builder.setInitializers(ExampleInitializer.class); 134 | ``` 135 | 136 | ### Root Services 137 | 138 | If interacting with a root shell is too limited for your needs, you can also implement a root service to run complex code. A root service is similar to [Bound Services](https://developer.android.com/guide/components/bound-services) but running in a root process. `libsu` uses Android's native IPC mechanism, binder, for communication between your root service and the main application process. In addition to running Java/Kotlin code, loading native libraries with JNI is also supported (`android:extractNativeLibs=false` **is** allowed). For more details, please read the full Javadoc of `RootService` and check out the example app for more details. Add `com.github.topjohnwu.libsu:service` as a dependency to access `RootService`: 139 | 140 | ```java 141 | public class RootConnection implements ServiceConnection { ... } 142 | public class ExampleService extends RootService { 143 | @Override 144 | public IBinder onBind(Intent intent) { 145 | // return IBinder from Messenger or AIDL stub implementation 146 | } 147 | } 148 | RootConnection connection = new RootConnection(); 149 | Intent intent = new Intent(context, ExampleService.class); 150 | RootService.bind(intent, connection); 151 | ``` 152 | 153 | ##### Debugging Root Services 154 | 155 | If the application process creating the root service has a debugger attached, the root service will automatically enable debugging mode and wait for the debugger to attach. In Android Studio, go to **"Run > Attach Debugger to Android Process"**, tick the **"Show all processes"** box, and you should be able to manually attach to the remote root process. Currently, only the **"Java only"** debugger is supported. 156 | 157 | ### I/O 158 | 159 | Add `com.github.topjohnwu.libsu:nio` as a dependency to access remote file system APIs: 160 | 161 | ```java 162 | // Create the file system service in the root process 163 | // For example, create and send the service back to the client in a RootService 164 | public class ExampleService extends RootService { 165 | @Override 166 | public IBinder onBind(Intent intent) { 167 | return FileSystemManager.getService(); 168 | } 169 | } 170 | 171 | // In the client process 172 | IBinder binder = /* From the root service connection */; 173 | FileSystemManager remoteFS; 174 | try { 175 | remoteFS = FileSystemManager.getRemote(binder); 176 | } catch (RemoteException e) { 177 | // Handle errors 178 | } 179 | ExtendedFile bootBlock = remoteFS.getFile("/dev/block/by-name/boot"); 180 | if (bootBlock.exists()) { 181 | ExtendedFile bootBackup = remoteFS.getFile("/data/boot.img"); 182 | try (InputStream in = bootBlock.newInputStream(); 183 | OutputStream out = bootBackup.newOutputStream()) { 184 | // Do I/O stuffs... 185 | } catch (IOException e) { 186 | // Handle errors 187 | } 188 | } 189 | ``` 190 | 191 | ## License 192 | 193 | This project is licensed under the Apache License, Version 2.0. Please refer to `LICENSE` for the full text. 194 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.android.build.gradle.BaseExtension 2 | import com.android.build.gradle.LibraryExtension 3 | import java.io.ByteArrayOutputStream 4 | import java.net.URL 5 | 6 | plugins { 7 | id("java") 8 | id("maven-publish") 9 | id("com.android.library") version "8.5.0" apply false 10 | } 11 | 12 | val dlPackageList by tasks.registering { 13 | outputs.upToDateWhen { false } 14 | doLast { 15 | // Merge framework packages with AndroidX packages into the same list 16 | // so links to Android classes can work properly in Javadoc 17 | 18 | val bos = ByteArrayOutputStream() 19 | URL("https://developer.android.com/reference/package-list") 20 | .openStream().use { src -> src.copyTo(bos) } 21 | URL("https://developer.android.com/reference/androidx/package-list") 22 | .openStream().use { src -> src.copyTo(bos) } 23 | 24 | // Strip out empty lines 25 | val packageList = bos.toString("UTF-8").replace("\n+".toRegex(), "\n") 26 | 27 | rootProject.layout.buildDirectory.asFile.get().mkdirs() 28 | rootProject.layout.buildDirectory.file("package-list").get().asFile.outputStream().use { 29 | it.writer().write(packageList) 30 | it.write("\n".toByteArray()) 31 | } 32 | } 33 | } 34 | 35 | val javadoc = (tasks["javadoc"] as Javadoc).apply { 36 | dependsOn(dlPackageList) 37 | isFailOnError = false 38 | title = "libsu API" 39 | exclude("**/internal/**") 40 | (options as StandardJavadocDocletOptions).apply { 41 | linksOffline = listOf(JavadocOfflineLink( 42 | "https://developer.android.com/reference/", 43 | rootProject.layout.buildDirectory.asFile.get().path)) 44 | isNoDeprecated = true 45 | addBooleanOption("-ignore-source-errors").value = true 46 | } 47 | setDestinationDir(rootProject.layout.buildDirectory.dir("javadoc").get().asFile) 48 | } 49 | 50 | val javadocJar by tasks.registering(Jar::class) { 51 | dependsOn(javadoc) 52 | archiveClassifier.set("javadoc") 53 | from(javadoc.destinationDir) 54 | } 55 | 56 | publishing { 57 | publications { 58 | create("maven") { 59 | artifact(javadocJar.get()) 60 | groupId = "com.github.topjohnwu" 61 | artifactId = "docs" 62 | } 63 | } 64 | } 65 | 66 | fun Project.android(configuration: BaseExtension.() -> Unit) = 67 | extensions.getByName("android").configuration() 68 | 69 | fun Project.androidLibrary(configuration: LibraryExtension.() -> Unit) = 70 | extensions.getByName("android").configuration() 71 | 72 | subprojects { 73 | configurations.create("javadocDeps") 74 | afterEvaluate { 75 | android { 76 | compileSdkVersion(34) 77 | buildToolsVersion = "34.0.0" 78 | 79 | defaultConfig { 80 | if (minSdkVersion == null) 81 | minSdk = 19 82 | targetSdk = 34 83 | } 84 | 85 | compileOptions { 86 | sourceCompatibility = JavaVersion.VERSION_1_8 87 | targetCompatibility = JavaVersion.VERSION_1_8 88 | } 89 | } 90 | 91 | if (plugins.hasPlugin("com.android.library")) { 92 | apply(plugin = "maven-publish") 93 | 94 | androidLibrary { 95 | buildFeatures { 96 | buildConfig = false 97 | } 98 | 99 | val sources = sourceSets.getByName("main").java.getSourceFiles() 100 | 101 | javadoc.apply { 102 | source += sources 103 | classpath += project.files(bootClasspath) 104 | classpath += configurations.getByName("javadocDeps") 105 | } 106 | 107 | publishing { 108 | singleVariant("release") { 109 | withSourcesJar() 110 | withJavadocJar() 111 | } 112 | } 113 | } 114 | 115 | publishing { 116 | publications { 117 | register("libsu") { 118 | afterEvaluate { 119 | from(components["release"]) 120 | } 121 | groupId = "com.github.topjohnwu.libsu" 122 | artifactId = project.name 123 | } 124 | } 125 | } 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /core/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /core/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.library") 3 | } 4 | 5 | group="com.github.topjohnwu.libsu" 6 | 7 | android { 8 | namespace = "com.topjohnwu.superuser" 9 | defaultConfig { 10 | consumerProguardFiles("proguard-rules.pro") 11 | } 12 | } 13 | 14 | dependencies { 15 | compileOnly("androidx.annotation:annotation:1.6.0") 16 | javadocDeps("androidx.annotation:annotation:1.6.0") 17 | } 18 | -------------------------------------------------------------------------------- /core/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | 23 | # Strip out debugging stuffs 24 | -assumenosideeffects class com.topjohnwu.superuser.internal.Utils { 25 | public static void log(...); 26 | public static void ex(...); 27 | public static boolean vLog() return false; 28 | public static boolean hasStartupAgents(android.content.Context) return false; 29 | } 30 | -assumenosideeffects class android.os.Debug { 31 | public static boolean isDebuggerConnected() return false; 32 | } 33 | 34 | # Make sure R8/Proguard don't break things 35 | -keep,allowobfuscation class * extends com.topjohnwu.superuser.Shell$Initializer { *; } 36 | -keep,allowobfuscation class * extends com.topjohnwu.superuser.ipc.RootService { *; } 37 | -------------------------------------------------------------------------------- /core/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /core/src/main/java/com/topjohnwu/superuser/CallbackList.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 John "topjohnwu" Wu 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.topjohnwu.superuser; 18 | 19 | import androidx.annotation.NonNull; 20 | import androidx.annotation.Nullable; 21 | 22 | import com.topjohnwu.superuser.internal.UiThreadHandler; 23 | 24 | import java.util.AbstractList; 25 | import java.util.List; 26 | import java.util.concurrent.Executor; 27 | 28 | /** 29 | * An {@link AbstractList} that calls {@code onAddElement} when a new element is added to the list. 30 | *

31 | * To simplify the API of {@link Shell}, both STDOUT and STDERR will output to {@link List}s. 32 | * This class is useful if you want to trigger a callback every time {@link Shell} 33 | * outputs a new line. 34 | *

35 | * The {@code CallbackList} itself does not have a data store. If you need one, you can provide a 36 | * base {@link List}, and this class will delegate its calls to it. 37 | */ 38 | 39 | public abstract class CallbackList extends AbstractList { 40 | 41 | protected List mBase; 42 | protected Executor mExecutor; 43 | 44 | /** 45 | * {@link #onAddElement(Object)} runs on the main thread; no backing list. 46 | */ 47 | protected CallbackList() { 48 | this(UiThreadHandler.executor, null); 49 | } 50 | 51 | /** 52 | * {@link #onAddElement(Object)} runs on the main thread; sets a backing list. 53 | */ 54 | protected CallbackList(@Nullable List base) { 55 | this(UiThreadHandler.executor, base); 56 | } 57 | 58 | /** 59 | * {@link #onAddElement(Object)} runs with the executor; no backing list. 60 | */ 61 | protected CallbackList(@NonNull Executor executor) { 62 | this(executor, null); 63 | } 64 | 65 | /** 66 | * {@link #onAddElement(Object)} runs with the executor; sets a backing list. 67 | */ 68 | protected CallbackList(@NonNull Executor executor, @Nullable List base) { 69 | mExecutor = executor; 70 | mBase = base; 71 | } 72 | 73 | /** 74 | * The callback when a new element is added. 75 | *

76 | * This method will be called after {@code add} is called. 77 | * Which thread it runs on depends on which constructor is used to construct the instance. 78 | * @param e the new element added to the list. 79 | */ 80 | public abstract void onAddElement(E e); 81 | 82 | /** 83 | * @see List#get(int) 84 | */ 85 | @Override 86 | public E get(int i) { 87 | return mBase == null ? null : mBase.get(i); 88 | } 89 | 90 | /** 91 | * @see List#set(int, Object) 92 | */ 93 | @Override 94 | public E set(int i, E s) { 95 | return mBase == null ? null : mBase.set(i, s); 96 | } 97 | 98 | /** 99 | * @see List#add(int, Object) 100 | */ 101 | @Override 102 | public void add(int i, E s) { 103 | if (mBase != null) 104 | mBase.add(i, s); 105 | mExecutor.execute(() -> onAddElement(s)); 106 | } 107 | 108 | /** 109 | * @see List#remove(Object) 110 | */ 111 | @Override 112 | public E remove(int i) { 113 | return mBase == null ? null : mBase.remove(i); 114 | } 115 | 116 | /** 117 | * @see List#size() 118 | */ 119 | @Override 120 | public int size() { 121 | return mBase == null ? 0 : mBase.size(); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /core/src/main/java/com/topjohnwu/superuser/NoShellException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 John "topjohnwu" Wu 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.topjohnwu.superuser; 18 | 19 | /** 20 | * Thrown when it is impossible to construct {@code Shell}. 21 | * This is a runtime exception, and should happen very rarely. 22 | */ 23 | 24 | public class NoShellException extends RuntimeException { 25 | 26 | public NoShellException(String msg) { 27 | super(msg); 28 | } 29 | 30 | public NoShellException(String message, Throwable cause) { 31 | super(message, cause); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /core/src/main/java/com/topjohnwu/superuser/ShellUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 John "topjohnwu" Wu 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.topjohnwu.superuser; 18 | 19 | import android.os.Looper; 20 | import android.text.TextUtils; 21 | 22 | import androidx.annotation.NonNull; 23 | 24 | import java.io.IOException; 25 | import java.io.InputStream; 26 | import java.util.ArrayList; 27 | import java.util.List; 28 | 29 | /** 30 | * Some handy utility methods that are used in {@code libsu}. 31 | *

32 | * These methods are for internal use. I personally find them pretty handy, so I gathered them here. 33 | * However, since these are meant to be used internally, they are not stable APIs. 34 | * I would change them without too much consideration if needed. Also, these methods are not well 35 | * tested for public usage, many might not handle some edge cases correctly. 36 | * You have been warned!! 37 | */ 38 | public final class ShellUtils { 39 | 40 | private ShellUtils() {} 41 | 42 | /** 43 | * Test whether the list is {@code null} or empty or all elements are empty strings. 44 | * @param out the output of a shell command. 45 | * @return {@code false} if the list is {@code null} or empty or all elements are empty strings. 46 | */ 47 | public static boolean isValidOutput(List out) { 48 | if (out != null && out.size() != 0) { 49 | // Check if all empty 50 | for (String s : out) 51 | if (!TextUtils.isEmpty(s)) 52 | return true; 53 | } 54 | return false; 55 | } 56 | 57 | /** 58 | * Run commands with the main shell and get a single line output. 59 | * @param cmds the commands. 60 | * @return the last line of the output of the command, empty string if no output is available. 61 | */ 62 | @NonNull 63 | public static String fastCmd(String... cmds) { 64 | return fastCmd(Shell.getShell(), cmds); 65 | } 66 | 67 | /** 68 | * Run commands and get a single line output. 69 | * @param shell a shell instance. 70 | * @param cmds the commands. 71 | * @return the last line of the output of the command, empty string if no output is available. 72 | */ 73 | @NonNull 74 | public static String fastCmd(Shell shell, String... cmds) { 75 | List out = shell.newJob().add(cmds).to(new ArrayList<>(), null).exec().getOut(); 76 | return isValidOutput(out) ? out.get(out.size() - 1) : ""; 77 | } 78 | 79 | /** 80 | * Run commands with the main shell and return whether exits with 0 (success). 81 | * @param cmds the commands. 82 | * @return {@code true} if the commands succeed. 83 | */ 84 | public static boolean fastCmdResult(String... cmds) { 85 | return fastCmdResult(Shell.getShell(), cmds); 86 | } 87 | 88 | /** 89 | * Run commands and return whether exits with 0 (success). 90 | * @param shell a shell instance. 91 | * @param cmds the commands. 92 | * @return {@code true} if the commands succeed. 93 | */ 94 | public static boolean fastCmdResult(Shell shell, String... cmds) { 95 | return shell.newJob().add(cmds).to(null).exec().isSuccess(); 96 | } 97 | 98 | /** 99 | * Check if current thread is main thread. 100 | * @return {@code true} if the current thread is the main thread. 101 | */ 102 | public static boolean onMainThread() { 103 | return Looper.getMainLooper().getThread() == Thread.currentThread(); 104 | } 105 | 106 | /** 107 | * Discard all data currently available in an {@link InputStream}. 108 | * @param in the {@link InputStream} to be cleaned. 109 | */ 110 | public static void cleanInputStream(InputStream in) { 111 | try { 112 | while (in.available() != 0) 113 | in.skip(in.available()); 114 | } catch (IOException ignored) {} 115 | } 116 | 117 | private static final char SINGLE_QUOTE = '\''; 118 | 119 | /** 120 | * Format string to quoted and escaped string suitable for shell commands. 121 | * @param s the string to be formatted. 122 | * @return the formatted string. 123 | */ 124 | public static String escapedString(String s) { 125 | StringBuilder sb = new StringBuilder(); 126 | sb.append(SINGLE_QUOTE); 127 | int len = s.length(); 128 | for (int i = 0; i < len; ++i) { 129 | char c = s.charAt(i); 130 | if (c == SINGLE_QUOTE) { 131 | sb.append("'\\''"); 132 | continue; 133 | } 134 | sb.append(c); 135 | } 136 | sb.append(SINGLE_QUOTE); 137 | return sb.toString(); 138 | } 139 | 140 | /** 141 | * Get the greatest common divisor of 2 integers with binary algorithm. 142 | * @param u an integer. 143 | * @param v an integer. 144 | * @return the greatest common divisor. 145 | */ 146 | public static long gcd(long u, long v) { 147 | if (u == 0) return v; 148 | if (v == 0) return u; 149 | 150 | int shift; 151 | for (shift = 0; ((u | v) & 1) == 0; ++shift) { 152 | u >>= 1; 153 | v >>= 1; 154 | } 155 | while ((u & 1) == 0) 156 | u >>= 1; 157 | do { 158 | while ((v & 1) == 0) 159 | v >>= 1; 160 | 161 | if (u > v) { 162 | long t = v; 163 | v = u; 164 | u = t; 165 | } 166 | v = v - u; 167 | } while (v != 0); 168 | 169 | return u << shift; 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /core/src/main/java/com/topjohnwu/superuser/internal/BuilderImpl.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 John "topjohnwu" Wu 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.topjohnwu.superuser.internal; 18 | 19 | import static com.topjohnwu.superuser.Shell.FLAG_MOUNT_MASTER; 20 | import static com.topjohnwu.superuser.Shell.FLAG_NON_ROOT_SHELL; 21 | import static com.topjohnwu.superuser.Shell.FLAG_REDIRECT_STDERR; 22 | 23 | import android.content.Context; 24 | import android.text.TextUtils; 25 | 26 | import androidx.annotation.NonNull; 27 | import androidx.annotation.RestrictTo; 28 | 29 | import com.topjohnwu.superuser.NoShellException; 30 | import com.topjohnwu.superuser.Shell; 31 | 32 | import java.io.IOException; 33 | import java.lang.reflect.Constructor; 34 | 35 | @RestrictTo(RestrictTo.Scope.LIBRARY) 36 | public final class BuilderImpl extends Shell.Builder { 37 | private static final String TAG = "BUILDER"; 38 | 39 | long timeout = 20; 40 | private int flags = 0; 41 | private Shell.Initializer[] initializers; 42 | private String[] command; 43 | 44 | boolean hasFlags(int mask) { 45 | return (flags & mask) == mask; 46 | } 47 | 48 | @NonNull 49 | @Override 50 | public Shell.Builder setFlags(int f) { 51 | flags = f; 52 | return this; 53 | } 54 | 55 | @NonNull 56 | @Override 57 | public Shell.Builder setTimeout(long t) { 58 | timeout = t; 59 | return this; 60 | } 61 | 62 | @NonNull 63 | @Override 64 | public Shell.Builder setCommands(String... c) { 65 | command = c; 66 | return this; 67 | } 68 | 69 | public void setInitializersImpl(Class[] clz) { 70 | initializers = new Shell.Initializer[clz.length]; 71 | for (int i = 0; i < clz.length; ++i) { 72 | try { 73 | Constructor c = clz[i].getDeclaredConstructor(); 74 | c.setAccessible(true); 75 | initializers[i] = c.newInstance(); 76 | } catch (ReflectiveOperationException | ClassCastException e) { 77 | Utils.err(e); 78 | } 79 | } 80 | } 81 | 82 | private ShellImpl start() { 83 | ShellImpl shell = null; 84 | 85 | // Root mount master 86 | if (!hasFlags(FLAG_NON_ROOT_SHELL) && hasFlags(FLAG_MOUNT_MASTER)) { 87 | try { 88 | shell = exec("su", "--mount-master"); 89 | if (!shell.isRoot()) 90 | shell = null; 91 | } catch (NoShellException ignore) {} 92 | } 93 | 94 | // Normal root shell 95 | if (shell == null && !hasFlags(FLAG_NON_ROOT_SHELL)) { 96 | try { 97 | shell = exec("su"); 98 | if (!shell.isRoot()) { 99 | shell = null; 100 | } 101 | } catch (NoShellException ignore) {} 102 | } 103 | 104 | // Try normal non-root shell 105 | if (shell == null) { 106 | if (!hasFlags(FLAG_NON_ROOT_SHELL)) { 107 | Utils.setConfirmedRootState(false); 108 | } 109 | shell = exec("sh"); 110 | } 111 | 112 | return shell; 113 | } 114 | 115 | private ShellImpl exec(String... commands) { 116 | try { 117 | Utils.log(TAG, "exec " + TextUtils.join(" ", commands)); 118 | Process process = Runtime.getRuntime().exec(commands); 119 | return build(process); 120 | } catch (IOException e) { 121 | Utils.ex(e); 122 | throw new NoShellException("Unable to create a shell!", e); 123 | } 124 | } 125 | 126 | @NonNull 127 | @Override 128 | public ShellImpl build(Process process) { 129 | ShellImpl shell; 130 | try { 131 | shell = new ShellImpl(this, process); 132 | } catch (IOException e) { 133 | Utils.ex(e); 134 | throw new NoShellException("Unable to create a shell!", e); 135 | } 136 | if (hasFlags(FLAG_REDIRECT_STDERR)) { 137 | Shell.enableLegacyStderrRedirection = true; 138 | } 139 | MainShell.setCached(shell); 140 | if (initializers != null) { 141 | Context ctx = Utils.getContext(); 142 | for (Shell.Initializer init : initializers) { 143 | if (init != null && !init.onInit(ctx, shell)) { 144 | MainShell.setCached(null); 145 | throw new NoShellException("Unable to init shell"); 146 | } 147 | } 148 | } 149 | return shell; 150 | } 151 | 152 | @NonNull 153 | @Override 154 | public ShellImpl build() { 155 | if (command != null) { 156 | return exec(command); 157 | } else { 158 | return start(); 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /core/src/main/java/com/topjohnwu/superuser/internal/JobTask.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 John "topjohnwu" Wu 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.topjohnwu.superuser.internal; 18 | 19 | import static com.topjohnwu.superuser.Shell.EXECUTOR; 20 | import static java.nio.charset.StandardCharsets.UTF_8; 21 | 22 | import androidx.annotation.NonNull; 23 | import androidx.annotation.Nullable; 24 | 25 | import com.topjohnwu.superuser.Shell; 26 | 27 | import java.io.IOException; 28 | import java.io.InputStream; 29 | import java.io.OutputStream; 30 | import java.util.ArrayList; 31 | import java.util.Collections; 32 | import java.util.List; 33 | import java.util.UUID; 34 | import java.util.concurrent.ExecutionException; 35 | import java.util.concurrent.Executor; 36 | import java.util.concurrent.FutureTask; 37 | 38 | abstract class JobTask extends Shell.Job implements Shell.Task { 39 | 40 | static final List UNSET_LIST = new ArrayList<>(0); 41 | 42 | static final String END_UUID = UUID.randomUUID().toString(); 43 | static final int UUID_LEN = 36; 44 | private static final byte[] END_CMD = String 45 | .format("__RET=$?;echo %1$s;echo %1$s >&2;echo $__RET;unset __RET\n", END_UUID) 46 | .getBytes(UTF_8); 47 | 48 | private final List sources = new ArrayList<>(); 49 | @Nullable private List out = null; 50 | @Nullable private List err = UNSET_LIST; 51 | 52 | @Nullable protected Executor callbackExecutor; 53 | @Nullable protected Shell.ResultCallback callback; 54 | 55 | private void setResult(@NonNull ResultImpl result) { 56 | if (callback != null) { 57 | if (callbackExecutor == null) 58 | callback.onResult(result); 59 | else 60 | callbackExecutor.execute(() -> callback.onResult(result)); 61 | } 62 | } 63 | 64 | private void close() { 65 | for (ShellInputSource src : sources) 66 | src.close(); 67 | } 68 | 69 | @Override 70 | public void run(@NonNull OutputStream stdin, 71 | @NonNull InputStream stdout, 72 | @NonNull InputStream stderr) { 73 | final boolean noOut = out == UNSET_LIST; 74 | final boolean noErr = err == UNSET_LIST; 75 | 76 | List outList = noOut ? (callback == null ? null : new ArrayList<>()) : out; 77 | List errList = noErr ? (Shell.enableLegacyStderrRedirection ? outList : null) : err; 78 | 79 | if (outList != null && outList == errList && !Utils.isSynchronized(outList)) { 80 | // Synchronize the list internally only if both lists are the same and are not 81 | // already synchronized by the user 82 | List list = Collections.synchronizedList(outList); 83 | outList = list; 84 | errList = list; 85 | } 86 | 87 | FutureTask outGobbler = new FutureTask<>(new StreamGobbler.OUT(stdout, outList)); 88 | FutureTask errGobbler = new FutureTask<>(new StreamGobbler.ERR(stderr, errList)); 89 | EXECUTOR.execute(outGobbler); 90 | EXECUTOR.execute(errGobbler); 91 | 92 | ResultImpl result = new ResultImpl(); 93 | try { 94 | for (ShellInputSource src : sources) 95 | src.serve(stdin); 96 | stdin.write(END_CMD); 97 | stdin.flush(); 98 | 99 | int code = outGobbler.get(); 100 | errGobbler.get(); 101 | 102 | result.code = code; 103 | result.out = outList; 104 | result.err = noErr ? null : err; 105 | } catch (IOException | ExecutionException | InterruptedException e) { 106 | Utils.err(e); 107 | } 108 | 109 | close(); 110 | setResult(result); 111 | } 112 | 113 | @Override 114 | public void shellDied() { 115 | close(); 116 | setResult(new ResultImpl()); 117 | } 118 | 119 | @NonNull 120 | @Override 121 | public Shell.Job to(List stdout) { 122 | out = stdout; 123 | err = UNSET_LIST; 124 | return this; 125 | } 126 | 127 | @NonNull 128 | @Override 129 | public Shell.Job to(List stdout, List stderr) { 130 | out = stdout; 131 | err = stderr; 132 | return this; 133 | } 134 | 135 | @NonNull 136 | @Override 137 | public Shell.Job add(@NonNull InputStream in) { 138 | if (in != null) 139 | sources.add(new InputStreamSource(in)); 140 | return this; 141 | } 142 | 143 | @NonNull 144 | @Override 145 | public Shell.Job add(@NonNull String... cmds) { 146 | if (cmds != null && cmds.length > 0) 147 | sources.add(new CommandSource(cmds)); 148 | return this; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /core/src/main/java/com/topjohnwu/superuser/internal/MainShell.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 John "topjohnwu" Wu 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.topjohnwu.superuser.internal; 18 | 19 | import static com.topjohnwu.superuser.Shell.EXECUTOR; 20 | import static com.topjohnwu.superuser.Shell.GetShellCallback; 21 | 22 | import androidx.annotation.GuardedBy; 23 | import androidx.annotation.RestrictTo; 24 | 25 | import com.topjohnwu.superuser.NoShellException; 26 | import com.topjohnwu.superuser.Shell; 27 | 28 | import java.io.InputStream; 29 | import java.util.concurrent.Executor; 30 | 31 | @RestrictTo(RestrictTo.Scope.LIBRARY) 32 | public final class MainShell { 33 | 34 | @GuardedBy("self") 35 | private static final ShellImpl[] mainShell = new ShellImpl[1]; 36 | 37 | @GuardedBy("class") 38 | private static boolean isInitMain; 39 | @GuardedBy("class") 40 | private static BuilderImpl mainBuilder; 41 | 42 | private MainShell() {} 43 | 44 | public static synchronized ShellImpl get() { 45 | ShellImpl shell = getCached(); 46 | if (shell == null) { 47 | if (isInitMain) { 48 | throw new NoShellException("The main shell died during initialization"); 49 | } 50 | isInitMain = true; 51 | if (mainBuilder == null) 52 | mainBuilder = new BuilderImpl(); 53 | shell = mainBuilder.build(); 54 | isInitMain = false; 55 | } 56 | return shell; 57 | } 58 | 59 | private static void returnShell(Shell s, Executor e, GetShellCallback cb) { 60 | if (e == null) 61 | cb.onShell(s); 62 | else 63 | e.execute(() -> cb.onShell(s)); 64 | } 65 | 66 | public static void get(Executor executor, GetShellCallback callback) { 67 | Shell shell = getCached(); 68 | if (shell != null) { 69 | returnShell(shell, executor, callback); 70 | } else { 71 | // Else we get shell in worker thread and call the callback when we get a Shell 72 | EXECUTOR.execute(() -> { 73 | try { 74 | returnShell(get(), executor, callback); 75 | } catch (NoShellException e) { 76 | Utils.ex(e); 77 | } 78 | }); 79 | } 80 | } 81 | 82 | public static ShellImpl getCached() { 83 | synchronized (mainShell) { 84 | ShellImpl s = mainShell[0]; 85 | if (s != null && s.getStatus() < 0) { 86 | s = null; 87 | mainShell[0] = null; 88 | } 89 | return s; 90 | } 91 | } 92 | 93 | static synchronized void setCached(ShellImpl shell) { 94 | if (isInitMain) { 95 | synchronized (mainShell) { 96 | mainShell[0] = shell; 97 | } 98 | } 99 | } 100 | 101 | public static synchronized void setBuilder(Shell.Builder builder) { 102 | if (isInitMain || getCached() != null) { 103 | throw new IllegalStateException("The main shell was already created"); 104 | } 105 | mainBuilder = (BuilderImpl) builder; 106 | } 107 | 108 | public static Shell.Job newJob(InputStream in) { 109 | return new PendingJob().add(in); 110 | } 111 | 112 | public static Shell.Job newJob(String... cmds) { 113 | return new PendingJob().add(cmds); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /core/src/main/java/com/topjohnwu/superuser/internal/PendingJob.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 John "topjohnwu" Wu 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.topjohnwu.superuser.internal; 18 | 19 | import androidx.annotation.NonNull; 20 | import androidx.annotation.Nullable; 21 | 22 | import com.topjohnwu.superuser.NoShellException; 23 | import com.topjohnwu.superuser.Shell; 24 | 25 | import java.io.IOException; 26 | import java.util.ArrayList; 27 | import java.util.concurrent.Executor; 28 | import java.util.concurrent.Future; 29 | 30 | class PendingJob extends JobTask { 31 | 32 | @Nullable 33 | private Runnable retryTask; 34 | 35 | PendingJob() { 36 | to(UNSET_LIST); 37 | } 38 | 39 | @Override 40 | public void shellDied() { 41 | if (retryTask != null) { 42 | Runnable r = retryTask; 43 | retryTask = null; 44 | r.run(); 45 | } else { 46 | super.shellDied(); 47 | } 48 | } 49 | 50 | private void exec0() { 51 | ShellImpl shell; 52 | try { 53 | shell = MainShell.get(); 54 | } catch (NoShellException e) { 55 | super.shellDied(); 56 | return; 57 | } 58 | try { 59 | shell.execTask(this); 60 | } catch (IOException ignored) { /* JobTask does not throw */ } 61 | } 62 | 63 | @NonNull 64 | @Override 65 | public Shell.Result exec() { 66 | retryTask = this::exec0; 67 | ResultHolder holder = new ResultHolder(); 68 | callback = holder; 69 | callbackExecutor = null; 70 | exec0(); 71 | return holder.getResult(); 72 | } 73 | 74 | private void submit0() { 75 | MainShell.get(null, s -> { 76 | ShellImpl shell = (ShellImpl) s; 77 | shell.submitTask(this); 78 | }); 79 | } 80 | 81 | @NonNull 82 | @Override 83 | public Future enqueue() { 84 | retryTask = this::submit0; 85 | ResultFuture future = new ResultFuture(); 86 | callback = future; 87 | callbackExecutor = null; 88 | submit0(); 89 | return future; 90 | } 91 | 92 | @Override 93 | public void submit(@Nullable Executor executor, @Nullable Shell.ResultCallback cb) { 94 | retryTask = this::submit0; 95 | callbackExecutor = executor; 96 | callback = cb; 97 | submit0(); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /core/src/main/java/com/topjohnwu/superuser/internal/ResultFuture.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 John "topjohnwu" Wu 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.topjohnwu.superuser.internal; 18 | 19 | import androidx.annotation.NonNull; 20 | 21 | import com.topjohnwu.superuser.Shell; 22 | 23 | import java.util.concurrent.CountDownLatch; 24 | import java.util.concurrent.Future; 25 | import java.util.concurrent.TimeUnit; 26 | import java.util.concurrent.TimeoutException; 27 | 28 | class ResultFuture extends ResultHolder implements Future { 29 | 30 | private final CountDownLatch latch = new CountDownLatch(1); 31 | 32 | @Override 33 | public void onResult(@NonNull Shell.Result out) { 34 | super.onResult(out); 35 | latch.countDown(); 36 | } 37 | 38 | @Override 39 | public boolean cancel(boolean mayInterruptIfRunning) { 40 | return latch.getCount() != 0; 41 | } 42 | 43 | @Override 44 | public boolean isCancelled() { 45 | return false; 46 | } 47 | 48 | @Override 49 | public boolean isDone() { 50 | return latch.getCount() == 0; 51 | } 52 | 53 | @Override 54 | public Shell.Result get() throws InterruptedException { 55 | latch.await(); 56 | return getResult(); 57 | } 58 | 59 | @Override 60 | public Shell.Result get(long timeout, TimeUnit unit) 61 | throws InterruptedException, TimeoutException { 62 | if (!latch.await(timeout, unit)) { 63 | throw new TimeoutException(); 64 | } 65 | return getResult(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /core/src/main/java/com/topjohnwu/superuser/internal/ResultHolder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 John "topjohnwu" Wu 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.topjohnwu.superuser.internal; 18 | 19 | import androidx.annotation.NonNull; 20 | import androidx.annotation.Nullable; 21 | 22 | import com.topjohnwu.superuser.Shell; 23 | 24 | class ResultHolder implements Shell.ResultCallback { 25 | 26 | @Nullable 27 | private Shell.Result result; 28 | 29 | @Override 30 | public void onResult(@NonNull Shell.Result out) { 31 | result = out; 32 | } 33 | 34 | @NonNull 35 | Shell.Result getResult() { 36 | return result == null ? new ResultImpl() : result; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /core/src/main/java/com/topjohnwu/superuser/internal/ResultImpl.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 John "topjohnwu" Wu 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.topjohnwu.superuser.internal; 18 | 19 | import androidx.annotation.NonNull; 20 | 21 | import com.topjohnwu.superuser.Shell; 22 | 23 | import java.util.Collections; 24 | import java.util.List; 25 | 26 | class ResultImpl extends Shell.Result { 27 | List out; 28 | List err; 29 | int code = JOB_NOT_EXECUTED; 30 | 31 | @NonNull 32 | @Override 33 | public List getOut() { 34 | return out == null ? Collections.emptyList() : out; 35 | } 36 | 37 | @NonNull 38 | @Override 39 | public List getErr() { 40 | return err == null ? Collections.emptyList() : err; 41 | } 42 | 43 | @Override 44 | public int getCode() { 45 | return code; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /core/src/main/java/com/topjohnwu/superuser/internal/ShellInputSource.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 John "topjohnwu" Wu 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.topjohnwu.superuser.internal; 18 | 19 | import static java.nio.charset.StandardCharsets.UTF_8; 20 | 21 | import java.io.Closeable; 22 | import java.io.IOException; 23 | import java.io.InputStream; 24 | import java.io.OutputStream; 25 | 26 | interface ShellInputSource extends Closeable { 27 | String TAG = "SHELL_IN"; 28 | 29 | void serve(OutputStream out) throws IOException; 30 | 31 | @Override 32 | default void close() {} 33 | } 34 | 35 | class InputStreamSource implements ShellInputSource { 36 | 37 | private final InputStream in; 38 | InputStreamSource(InputStream in) { this.in = in; } 39 | 40 | @Override 41 | public void serve(OutputStream out) throws IOException { 42 | Utils.pump(in, out); 43 | in.close(); 44 | out.write('\n'); 45 | Utils.log(TAG, ""); 46 | } 47 | 48 | @Override 49 | public void close() { 50 | try { 51 | in.close(); 52 | } catch (IOException ignored) {} 53 | } 54 | } 55 | 56 | class CommandSource implements ShellInputSource { 57 | 58 | private final String[] cmd; 59 | CommandSource(String[] cmd) { this.cmd = cmd; } 60 | 61 | @Override 62 | public void serve(OutputStream out) throws IOException { 63 | for (String command : cmd) { 64 | out.write(command.getBytes(UTF_8)); 65 | out.write('\n'); 66 | Utils.log(TAG, command); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /core/src/main/java/com/topjohnwu/superuser/internal/ShellJob.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 John "topjohnwu" Wu 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.topjohnwu.superuser.internal; 18 | 19 | import androidx.annotation.NonNull; 20 | import androidx.annotation.Nullable; 21 | 22 | import com.topjohnwu.superuser.Shell; 23 | 24 | import java.io.IOException; 25 | import java.util.concurrent.Executor; 26 | import java.util.concurrent.Future; 27 | 28 | class ShellJob extends JobTask { 29 | 30 | @NonNull 31 | private final ShellImpl shell; 32 | 33 | ShellJob(@NonNull ShellImpl s) { 34 | shell = s; 35 | } 36 | 37 | @NonNull 38 | @Override 39 | public Shell.Result exec() { 40 | ResultHolder holder = new ResultHolder(); 41 | callback = holder; 42 | callbackExecutor = null; 43 | try { 44 | shell.execTask(this); 45 | } catch (IOException ignored) { /* JobTask does not throw */ } 46 | return holder.getResult(); 47 | } 48 | 49 | @Override 50 | public void submit(@Nullable Executor executor, @Nullable Shell.ResultCallback cb) { 51 | callbackExecutor = executor; 52 | callback = cb; 53 | shell.submitTask(this); 54 | } 55 | 56 | @NonNull 57 | @Override 58 | public Future enqueue() { 59 | ResultFuture future = new ResultFuture(); 60 | callback = future; 61 | callbackExecutor = null; 62 | shell.submitTask(this); 63 | return future; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /core/src/main/java/com/topjohnwu/superuser/internal/StreamGobbler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 John "topjohnwu" Wu 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.topjohnwu.superuser.internal; 18 | 19 | import static com.topjohnwu.superuser.internal.JobTask.END_UUID; 20 | import static com.topjohnwu.superuser.internal.JobTask.UUID_LEN; 21 | import static java.nio.charset.StandardCharsets.UTF_8; 22 | 23 | import java.io.BufferedReader; 24 | import java.io.IOException; 25 | import java.io.InputStream; 26 | import java.io.InputStreamReader; 27 | import java.util.List; 28 | import java.util.concurrent.Callable; 29 | 30 | abstract class StreamGobbler implements Callable { 31 | 32 | private static final String TAG = "SHELLOUT"; 33 | 34 | protected final InputStream in; 35 | protected final List list; 36 | 37 | StreamGobbler(InputStream in, List list) { 38 | this.in = in; 39 | this.list = list; 40 | } 41 | 42 | private boolean outputAndCheck(String line) { 43 | if (line == null) 44 | return false; 45 | 46 | int len = line.length(); 47 | boolean end = line.startsWith(END_UUID, len - UUID_LEN); 48 | if (end) { 49 | if (len == UUID_LEN) 50 | return false; 51 | line = line.substring(0, len - UUID_LEN); 52 | } 53 | if (list != null) { 54 | list.add(line); 55 | Utils.log(TAG, line); 56 | } 57 | return !end; 58 | } 59 | 60 | protected String process(boolean res) throws IOException { 61 | BufferedReader br = new BufferedReader(new InputStreamReader(in, UTF_8)); 62 | String line; 63 | do { 64 | line = br.readLine(); 65 | } while (outputAndCheck(line)); 66 | return res ? br.readLine() : null; 67 | } 68 | 69 | static class OUT extends StreamGobbler { 70 | 71 | private static final int NO_RESULT_CODE = 1; 72 | 73 | OUT(InputStream in, List list) { super(in, list); } 74 | 75 | @Override 76 | public Integer call() throws Exception { 77 | String codeStr = process(true); 78 | try { 79 | int code = codeStr == null ? NO_RESULT_CODE : Integer.parseInt(codeStr); 80 | Utils.log(TAG, "(exit code: " + code + ")"); 81 | return code; 82 | } catch (NumberFormatException e) { 83 | return NO_RESULT_CODE; 84 | } 85 | } 86 | } 87 | 88 | static class ERR extends StreamGobbler { 89 | 90 | ERR(InputStream in, List list) { super(in, list); } 91 | 92 | @Override 93 | public Void call() throws Exception { 94 | process(false); 95 | return null; 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /core/src/main/java/com/topjohnwu/superuser/internal/UiThreadHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 John "topjohnwu" Wu 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.topjohnwu.superuser.internal; 18 | 19 | import android.os.Handler; 20 | import android.os.Looper; 21 | 22 | import com.topjohnwu.superuser.ShellUtils; 23 | 24 | import java.util.concurrent.Executor; 25 | 26 | public class UiThreadHandler { 27 | public static final Handler handler = new Handler(Looper.getMainLooper()); 28 | public static final Executor executor = UiThreadHandler::run; 29 | public static void run(Runnable r) { 30 | if (ShellUtils.onMainThread()) { 31 | r.run(); 32 | } else { 33 | handler.post(r); 34 | } 35 | } 36 | 37 | public static void runAndWait(Runnable r) { 38 | if (ShellUtils.onMainThread()) { 39 | r.run(); 40 | } else { 41 | WaitRunnable wr = new WaitRunnable(r); 42 | handler.post(wr); 43 | wr.waitUntilDone(); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /core/src/main/java/com/topjohnwu/superuser/internal/Utils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 John "topjohnwu" Wu 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.topjohnwu.superuser.internal; 18 | 19 | import android.annotation.SuppressLint; 20 | import android.content.Context; 21 | import android.content.ContextWrapper; 22 | import android.os.Build; 23 | import android.os.Process; 24 | import android.util.ArraySet; 25 | import android.util.Log; 26 | 27 | import androidx.annotation.RestrictTo; 28 | 29 | import com.topjohnwu.superuser.Shell; 30 | 31 | import java.io.File; 32 | import java.io.IOException; 33 | import java.io.InputStream; 34 | import java.io.OutputStream; 35 | import java.lang.reflect.Method; 36 | import java.util.Collection; 37 | import java.util.Collections; 38 | import java.util.HashSet; 39 | import java.util.Objects; 40 | import java.util.Set; 41 | 42 | @RestrictTo(RestrictTo.Scope.LIBRARY) 43 | public final class Utils { 44 | 45 | private static Class synchronizedCollectionClass; 46 | private static final String TAG = "LIBSU"; 47 | 48 | // -1: uninitialized 49 | // 0: checked, no root 50 | // 1: checked, undetermined 51 | // 2: checked, root access 52 | private static int currentRootState = -1; 53 | 54 | @SuppressLint("StaticFieldLeak") 55 | public static Context context; 56 | 57 | public static void log(Object log) { 58 | log(TAG, log); 59 | } 60 | 61 | public static void log(String tag, Object log) { 62 | if (vLog()) 63 | Log.d(tag, log.toString()); 64 | } 65 | 66 | public static void ex(Throwable t) { 67 | if (vLog()) 68 | Log.d(TAG, "", t); 69 | } 70 | 71 | public static void err(Throwable t) { 72 | err(TAG, t); 73 | } 74 | 75 | public static void err(String tag, Throwable t) { 76 | Log.d(tag, "", t); 77 | } 78 | 79 | public static boolean vLog() { 80 | return Shell.enableVerboseLogging; 81 | } 82 | 83 | public static void setContext(Context c) { 84 | // Get the ContextImpl first so that getApplicationContext cannot be overridden 85 | c = Utils.getContextImpl(c); 86 | // Then get the application context, as the provided context could be from 87 | // a provider, receiver, service, or activity. 88 | Context app = c.getApplicationContext(); 89 | // getApplicationContext() could return null if the context is provided 90 | // during the Application's attach or some other non-standard situation. 91 | if (app != null) 92 | c = app; 93 | // Finally, get the raw ContextImpl of the app. 94 | context = Utils.getContextImpl(c); 95 | } 96 | 97 | @SuppressLint("PrivateApi") 98 | public static Context getContext() { 99 | if (context == null) { 100 | // Fetching ActivityThread on the main thread is no longer required on API 18+ 101 | // See: https://cs.android.com/android/platform/frameworks/base/+/66a017b63461a22842b3678c9520f803d5ddadfc 102 | try { 103 | Context c = (Context) Class.forName("android.app.ActivityThread") 104 | .getMethod("currentApplication") 105 | .invoke(null); 106 | context = Utils.getContextImpl(c); 107 | } catch (Exception e) { 108 | // Shall never happen 109 | Utils.err(e); 110 | } 111 | } 112 | return context; 113 | } 114 | 115 | public static Context getDeContext() { 116 | Context ctx = getContext(); 117 | return Build.VERSION.SDK_INT >= 24 ? ctx.createDeviceProtectedStorageContext() : ctx; 118 | } 119 | 120 | public static Context getContextImpl(Context context) { 121 | while (context instanceof ContextWrapper) { 122 | context = ((ContextWrapper) context).getBaseContext(); 123 | } 124 | return context; 125 | } 126 | 127 | public static boolean hasStartupAgents(Context context) { 128 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) 129 | return false; 130 | File agents = new File(context.getCodeCacheDir(), "startup_agents"); 131 | return agents.isDirectory(); 132 | } 133 | 134 | public static boolean isSynchronized(Collection collection) { 135 | if (synchronizedCollectionClass == null) { 136 | synchronizedCollectionClass = 137 | Collections.synchronizedCollection(Collections.emptyList()).getClass(); 138 | } 139 | return synchronizedCollectionClass.isInstance(collection); 140 | } 141 | 142 | public static long pump(InputStream in, OutputStream out) throws IOException { 143 | int read; 144 | long total = 0; 145 | byte[] buf = new byte[64 * 1024]; /* 64K buffer */ 146 | while ((read = in.read(buf)) > 0) { 147 | out.write(buf, 0, read); 148 | total += read; 149 | } 150 | return total; 151 | } 152 | 153 | static Set newArraySet() { 154 | if (Build.VERSION.SDK_INT >= 23) { 155 | return new ArraySet<>(); 156 | } else { 157 | return new HashSet<>(); 158 | } 159 | } 160 | 161 | public synchronized static Boolean isAppGrantedRoot() { 162 | if (currentRootState < 0) { 163 | if (Process.myUid() == 0) { 164 | // The current process is a root service 165 | currentRootState = 2; 166 | return true; 167 | } 168 | // noinspection ConstantConditions 169 | for (String path : System.getenv("PATH").split(":")) { 170 | File su = new File(path, "su"); 171 | if (su.canExecute()) { 172 | // We don't actually know whether the app has been granted root access. 173 | // Do NOT set the value as a confirmed state. 174 | currentRootState = 1; 175 | return null; 176 | } 177 | } 178 | currentRootState = 0; 179 | return false; 180 | } 181 | switch (currentRootState) { 182 | case 0 : return false; 183 | case 2 : return true; 184 | default: return null; 185 | } 186 | } 187 | 188 | synchronized static void setConfirmedRootState(boolean value) { 189 | currentRootState = value ? 2 : 0; 190 | } 191 | 192 | public static boolean isRootImpossible() { 193 | return Objects.equals(isAppGrantedRoot(), Boolean.FALSE); 194 | } 195 | 196 | public static boolean isMainShellRoot() { 197 | return MainShell.get().isRoot(); 198 | } 199 | 200 | @SuppressLint("DiscouragedPrivateApi") 201 | public static boolean isProcess64Bit() { 202 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 203 | return Process.is64Bit(); 204 | } 205 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { 206 | return false; 207 | } 208 | try { 209 | Class classVMRuntime = Class.forName("dalvik.system.VMRuntime"); 210 | Method getRuntime = classVMRuntime.getDeclaredMethod("getRuntime"); 211 | getRuntime.setAccessible(true); 212 | Object runtime = getRuntime.invoke(null); 213 | Method is64Bit = classVMRuntime.getDeclaredMethod("is64Bit"); 214 | is64Bit.setAccessible(true); 215 | // noinspection ConstantConditions 216 | return (boolean) is64Bit.invoke(runtime); 217 | } catch (ReflectiveOperationException e) { 218 | err(e); 219 | return false; 220 | } 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /core/src/main/java/com/topjohnwu/superuser/internal/WaitRunnable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 John "topjohnwu" Wu 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.topjohnwu.superuser.internal; 18 | 19 | import androidx.annotation.NonNull; 20 | 21 | public final class WaitRunnable implements Runnable { 22 | 23 | private Runnable r; 24 | 25 | public WaitRunnable(@NonNull Runnable run) { 26 | r = run; 27 | } 28 | 29 | public synchronized void waitUntilDone() { 30 | while (r != null) { 31 | try { 32 | wait(); 33 | } catch (InterruptedException ignored) {} 34 | } 35 | } 36 | 37 | @Override 38 | public synchronized void run() { 39 | r.run(); 40 | r = null; 41 | notifyAll(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /.cxx 3 | -------------------------------------------------------------------------------- /example/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | } 4 | 5 | android { 6 | namespace = "com.topjohnwu.libsuexample" 7 | 8 | defaultConfig { 9 | minSdk = 21 10 | applicationId = "com.topjohnwu.libsuexample" 11 | versionCode = 1 12 | versionName ="1.0" 13 | } 14 | 15 | buildFeatures { 16 | buildConfig = true 17 | viewBinding = true 18 | aidl = true 19 | } 20 | 21 | buildTypes { 22 | getByName("release") { 23 | isMinifyEnabled = true 24 | isShrinkResources = true 25 | proguardFiles( 26 | getDefaultProguardFile("proguard-android-optimize.txt"), 27 | "proguard-rules.pro" 28 | ) 29 | } 30 | } 31 | externalNativeBuild { 32 | cmake { 33 | path = file("src/main/cpp/CMakeLists.txt") 34 | } 35 | } 36 | 37 | lint { 38 | abortOnError = false 39 | } 40 | } 41 | 42 | dependencies { 43 | implementation("androidx.annotation:annotation:1.6.0") 44 | implementation(project(":core")) 45 | implementation(project(":service")) 46 | implementation(project(":io")) 47 | implementation(project(":nio")) 48 | } 49 | -------------------------------------------------------------------------------- /example/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | 23 | -repackageclasses 24 | -allowaccessmodification 25 | -------------------------------------------------------------------------------- /example/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /example/src/main/aidl/com/topjohnwu/libsuexample/ITestService.aidl: -------------------------------------------------------------------------------- 1 | // ITestService.aidl 2 | package com.topjohnwu.libsuexample; 3 | 4 | // Declare any non-default types here with import statements 5 | 6 | interface ITestService { 7 | int getPid(); 8 | int getUid(); 9 | String getUUID(); 10 | IBinder getFileSystemService(); 11 | } 12 | -------------------------------------------------------------------------------- /example/src/main/cpp/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Sets the minimum version of CMake required to build your native library. 2 | # This ensures that a certain set of CMake features is available to 3 | # your build. 4 | 5 | cmake_minimum_required(VERSION 3.4.1) 6 | 7 | # Specifies a library name, specifies whether the library is STATIC or 8 | # SHARED, and provides relative paths to the source code. You can 9 | # define multiple libraries by adding multiple add_library() commands, 10 | # and CMake builds them for you. When you build your app, Gradle 11 | # automatically packages shared libraries with your APK. 12 | 13 | add_library(native-lib SHARED 14 | test.cpp) 15 | -------------------------------------------------------------------------------- /example/src/main/cpp/test.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 John "topjohnwu" Wu 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #include 18 | #include 19 | 20 | extern "C" JNIEXPORT JNICALL 21 | jint Java_com_topjohnwu_libsuexample_AIDLService_nativeGetUid( 22 | JNIEnv *env, jobject instance) { 23 | return getuid(); 24 | } 25 | -------------------------------------------------------------------------------- /example/src/main/java/com/topjohnwu/libsuexample/AIDLService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 John "topjohnwu" Wu 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.topjohnwu.libsuexample; 18 | 19 | import static com.topjohnwu.libsuexample.MainActivity.TAG; 20 | 21 | import android.content.Intent; 22 | import android.os.IBinder; 23 | import android.os.Process; 24 | import android.util.Log; 25 | 26 | import androidx.annotation.NonNull; 27 | 28 | import com.topjohnwu.superuser.ipc.RootService; 29 | import com.topjohnwu.superuser.nio.FileSystemManager; 30 | 31 | import java.util.UUID; 32 | 33 | // Demonstrate RootService using AIDL (daemon mode) 34 | class AIDLService extends RootService { 35 | 36 | static { 37 | // Only load the library when this class is loaded in a root process. 38 | // The classloader will load this class (and call this static block) in the non-root 39 | // process because we accessed it when constructing the Intent to send. 40 | // Add this check so we don't unnecessarily load native code that'll never be used. 41 | if (Process.myUid() == 0) 42 | System.loadLibrary("native-lib"); 43 | } 44 | 45 | // Demonstrate we can also run native code via JNI with RootServices 46 | native int nativeGetUid(); 47 | 48 | class TestIPC extends ITestService.Stub { 49 | @Override 50 | public int getPid() { 51 | return Process.myPid(); 52 | } 53 | 54 | @Override 55 | public int getUid() { 56 | return nativeGetUid(); 57 | } 58 | 59 | @Override 60 | public String getUUID() { 61 | return uuid; 62 | } 63 | 64 | @Override 65 | public IBinder getFileSystemService() { 66 | return FileSystemManager.getService(); 67 | } 68 | } 69 | 70 | private final String uuid = UUID.randomUUID().toString(); 71 | 72 | @Override 73 | public void onCreate() { 74 | Log.d(TAG, "AIDLService: onCreate, " + uuid); 75 | } 76 | 77 | @Override 78 | public void onRebind(@NonNull Intent intent) { 79 | // This callback will be called when we are reusing a previously started root process 80 | Log.d(TAG, "AIDLService: onRebind, daemon process reused"); 81 | } 82 | 83 | @Override 84 | public IBinder onBind(@NonNull Intent intent) { 85 | Log.d(TAG, "AIDLService: onBind"); 86 | return new TestIPC(); 87 | } 88 | 89 | @Override 90 | public boolean onUnbind(@NonNull Intent intent) { 91 | Log.d(TAG, "AIDLService: onUnbind, client process unbound"); 92 | // Return true here so onRebind will be called 93 | return true; 94 | } 95 | 96 | @Override 97 | public void onDestroy() { 98 | Log.d(TAG, "AIDLService: onDestroy"); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /example/src/main/java/com/topjohnwu/libsuexample/MSGService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 John "topjohnwu" Wu 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.topjohnwu.libsuexample; 18 | 19 | import static com.topjohnwu.libsuexample.MainActivity.TAG; 20 | 21 | import android.content.Intent; 22 | import android.os.Bundle; 23 | import android.os.Handler; 24 | import android.os.IBinder; 25 | import android.os.Looper; 26 | import android.os.Message; 27 | import android.os.Messenger; 28 | import android.os.Process; 29 | import android.os.RemoteException; 30 | import android.util.Log; 31 | 32 | import androidx.annotation.NonNull; 33 | 34 | import com.topjohnwu.superuser.ipc.RootService; 35 | 36 | import java.util.UUID; 37 | 38 | // Demonstrate root service using Messengers 39 | class MSGService extends RootService implements Handler.Callback { 40 | 41 | static final int MSG_GETINFO = 1; 42 | static final int MSG_STOP = 2; 43 | static final String UUID_KEY = "uuid"; 44 | 45 | private String uuid; 46 | 47 | @Override 48 | public void onCreate() { 49 | uuid = UUID.randomUUID().toString(); 50 | Log.d(TAG, "MSGService: onCreate, " + uuid); 51 | } 52 | 53 | @Override 54 | public IBinder onBind(@NonNull Intent intent) { 55 | Log.d(TAG, "MSGService: onBind"); 56 | Handler h = new Handler(Looper.getMainLooper(), this); 57 | Messenger m = new Messenger(h); 58 | return m.getBinder(); 59 | } 60 | 61 | @Override 62 | public boolean handleMessage(@NonNull Message msg) { 63 | if (msg.what == MSG_STOP) { 64 | stopSelf(); 65 | return false; 66 | } 67 | if (msg.what != MSG_GETINFO) 68 | return false; 69 | Message reply = Message.obtain(); 70 | reply.what = msg.what; 71 | reply.arg1 = Process.myPid(); 72 | reply.arg2 = Process.myUid(); 73 | Bundle data = new Bundle(); 74 | data.putString(UUID_KEY, uuid); 75 | reply.setData(data); 76 | try { 77 | msg.replyTo.send(reply); 78 | } catch (RemoteException e) { 79 | Log.e(TAG, "Remote error", e); 80 | } 81 | return false; 82 | } 83 | 84 | @Override 85 | public boolean onUnbind(@NonNull Intent intent) { 86 | Log.d(TAG, "MSGService: onUnbind, client process unbound"); 87 | // Default returns false, which means onRebind will not be called 88 | return false; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /example/src/main/java/com/topjohnwu/libsuexample/StressTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 John "topjohnwu" Wu 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.topjohnwu.libsuexample; 18 | 19 | import static com.topjohnwu.libsuexample.MainActivity.TAG; 20 | import static com.topjohnwu.superuser.nio.FileSystemManager.MODE_READ_ONLY; 21 | import static com.topjohnwu.superuser.nio.FileSystemManager.MODE_WRITE_ONLY; 22 | 23 | import android.util.Log; 24 | 25 | import com.topjohnwu.superuser.Shell; 26 | import com.topjohnwu.superuser.io.SuFile; 27 | import com.topjohnwu.superuser.io.SuRandomAccessFile; 28 | import com.topjohnwu.superuser.nio.ExtendedFile; 29 | import com.topjohnwu.superuser.nio.FileSystemManager; 30 | 31 | import java.io.InputStream; 32 | import java.io.OutputStream; 33 | import java.nio.ByteBuffer; 34 | import java.nio.channels.FileChannel; 35 | import java.security.MessageDigest; 36 | import java.security.NoSuchAlgorithmException; 37 | import java.util.Arrays; 38 | import java.util.HashMap; 39 | import java.util.Map; 40 | import java.util.Random; 41 | 42 | public class StressTest { 43 | 44 | interface FileCallback { 45 | void onFile(ExtendedFile file) throws Exception; 46 | } 47 | 48 | private static final String TEST_DIR= "/system/app"; 49 | private static final int BUFFER_SIZE = 512 * 1024; 50 | private static final Random r = new Random(); 51 | private static final MessageDigest md; 52 | 53 | static { 54 | MessageDigest m; 55 | try { 56 | m = MessageDigest.getInstance("SHA-256"); 57 | } catch (NoSuchAlgorithmException e) { 58 | m = null; 59 | } 60 | md = m; 61 | } 62 | 63 | private static FileSystemManager remoteFS; 64 | private static FileCallback callback; 65 | private static Map hashes; 66 | 67 | public static void perform(FileSystemManager fs) { 68 | remoteFS = fs; 69 | Shell.EXECUTOR.execute(() -> { 70 | try { 71 | collectHashes(); 72 | // Test I/O streams 73 | testShellStream(); 74 | testRemoteStream(); 75 | // Test random I/O 76 | testShellRandomIO(); 77 | testRemoteChannel(); 78 | } catch (Exception e){ 79 | Log.d(TAG, "", e); 80 | } finally { 81 | cancel(); 82 | } 83 | }); 84 | } 85 | 86 | public static void cancel() { 87 | // These shall force tons of exceptions and cancel the thread :) 88 | callback = null; 89 | remoteFS = null; 90 | hashes = null; 91 | } 92 | 93 | private static void collectHashes() throws Exception { 94 | FileSystemManager fs = FileSystemManager.getLocal(); 95 | ExtendedFile root = fs.getFile(TEST_DIR); 96 | 97 | // Collect checksums of all files in test dir and use it as a reference 98 | // to verify the correctness of the several I/O implementations. 99 | Map map = new HashMap<>(); 100 | byte[] buf = new byte[BUFFER_SIZE]; 101 | callback = file -> { 102 | try (InputStream in = file.newInputStream()) { 103 | for (;;) { 104 | int read = in.read(buf); 105 | if (read <= 0) 106 | break; 107 | md.update(buf, 0, read); 108 | } 109 | } 110 | map.put(file.getPath(), md.digest()); 111 | }; 112 | traverse(root); 113 | hashes = map; 114 | } 115 | 116 | private static void testShellStream() throws Exception { 117 | SuFile root = new SuFile(TEST_DIR); 118 | SuFile outFile = new SuFile("/dev/null"); 119 | testIOStream(root, outFile); 120 | } 121 | 122 | private static void testRemoteStream() throws Exception { 123 | ExtendedFile root = remoteFS.getFile(TEST_DIR); 124 | ExtendedFile outFile = remoteFS.getFile("/dev/null"); 125 | testIOStream(root, outFile); 126 | } 127 | 128 | private static void testIOStream(ExtendedFile root, ExtendedFile outFile) throws Exception { 129 | OutputStream out = outFile.newOutputStream(); 130 | byte[] buf = new byte[BUFFER_SIZE]; 131 | callback = file -> { 132 | Log.d(TAG, file.getClass().getSimpleName() + " stream: " + file.getPath()); 133 | try (InputStream in = file.newInputStream()) { 134 | for (;;) { 135 | int read = in.read(buf); 136 | if (read <= 0) 137 | break; 138 | out.write(buf, 0, read); 139 | md.update(buf, 0, read); 140 | } 141 | out.flush(); 142 | } 143 | }; 144 | try { 145 | traverse(root); 146 | } finally { 147 | out.close(); 148 | } 149 | } 150 | 151 | private static void testShellRandomIO() throws Exception { 152 | SuFile root = new SuFile(TEST_DIR); 153 | 154 | OutputStream out = new SuFile("/dev/null").newOutputStream(); 155 | byte[] buf = new byte[BUFFER_SIZE]; 156 | callback = file -> { 157 | Log.d(TAG, "SuRandomAccessFile: " + file.getPath()); 158 | try (SuRandomAccessFile in = SuRandomAccessFile.open(file, "r")) { 159 | for (;;) { 160 | // Randomize read/write length to test unaligned I/O 161 | int len = r.nextInt(buf.length); 162 | int read = in.read(buf, 0, len); 163 | if (read <= 0) 164 | break; 165 | out.write(buf, 0, read); 166 | md.update(buf, 0, read); 167 | } 168 | out.flush(); 169 | } 170 | }; 171 | try { 172 | traverse(root); 173 | } finally { 174 | out.close(); 175 | } 176 | } 177 | 178 | private static void testRemoteChannel() throws Exception { 179 | ExtendedFile root = remoteFS.getFile(TEST_DIR); 180 | 181 | FileChannel out = remoteFS.openChannel("/dev/null", MODE_WRITE_ONLY); 182 | ByteBuffer buf = ByteBuffer.allocateDirect(BUFFER_SIZE); 183 | callback = file -> { 184 | Log.d(TAG, "RemoteFileChannel: " + file.getPath()); 185 | try (FileChannel src = remoteFS.openChannel(file, MODE_READ_ONLY)) { 186 | for (;;) { 187 | // Randomize read/write length 188 | int len = r.nextInt(buf.capacity()); 189 | buf.limit(len); 190 | if (src.read(buf) <= 0) 191 | break; 192 | buf.flip(); 193 | out.write(buf); 194 | buf.rewind(); 195 | md.update(buf); 196 | buf.clear(); 197 | } 198 | } 199 | }; 200 | try { 201 | traverse(root); 202 | } finally { 203 | out.close(); 204 | } 205 | } 206 | 207 | private static void verifyHash(ExtendedFile file) { 208 | if (hashes == null) 209 | return; 210 | byte[] refHash = hashes.get(file.getPath()); 211 | if (refHash == null) { 212 | Log.e(TAG, "ref hash is null: " + file.getPath()); 213 | } else if (!Arrays.equals(refHash, md.digest())) { 214 | Log.e(TAG, file.getClass().getSimpleName() + 215 | " hash mismatch: " + file.getPath()); 216 | } 217 | } 218 | 219 | private static void traverse(ExtendedFile file) throws Exception { 220 | if (file.isSymlink()) 221 | return; 222 | if (file.isDirectory()) { 223 | ExtendedFile[] ls = file.listFiles(); 224 | if (ls == null) 225 | return; 226 | for (ExtendedFile child : ls) { 227 | traverse(child); 228 | } 229 | } else { 230 | md.reset(); 231 | callback.onFile(file); 232 | verifyHash(file); 233 | } 234 | } 235 | 236 | } 237 | -------------------------------------------------------------------------------- /example/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 16 | 17 | 20 | 21 | 29 | 30 | 31 | 32 | 33 | 34 | 40 | 41 |