2013年5月5日 星期日

How-To SU


0. Table of Contents

0     Table of Contents

1     Introduction

2     Code: libsuperuser

3     How to call su
3.1     Common pitfalls
3.2     Making the call
3.3     Checking for su availability
3.4     Checking for su version

4     When to call su
4.1     When not to call su
4.2     Detecting the main thread
4.3     Using AsyncTask
4.4     Using IntentService

5     Updates
5.1     Gobbling (Dec 17, 2012)
5.2     Full content logging (Jan 23, 2013)
5.3     Parameters for su (Jan 23, 2013)
5.4     ACCESS_SUPERUSER permission (Feb 28, 2013)

1. Introduction

Since I started writing SuperSU, I have run into a lot of implementation problems. Problems with my own code in SuperSU, undocumented oddities in Android, and problems in other people's apps requiring root. Over time I've gone through a lot of app's source codes (or reversed the binaries) to figure out the root of the problems, and worked with various app authors (from the unknown to the famous) to fix them.
Due to those efforts, it has become clear that most freezes and crashes related to su access - both with SuperSU as well as Superuser - originate from two core problems: how su is called, and when su is called. These cases are not as straightforward as they may sound.
I have been asked by a number of developers to write this guide to provide guidelines and further information on how to get around all these problems - and that is what you are reading. As this is important to all root apps, I have also asked Adam "ChainsDD" Shanks (author of Superuser) to review this document, which he has done.
I don't expect this to be the final word on the matter, or that the code samples will be perfect. I hope this document and the code will provide you the insights needed, and generally be a good start.
- Jorrit "Chainfire" Jongma, author of SuperSU

2. Code: libsuperuser


There is source code accompanying this document, in the form of [libsuperuser @ GitHub] (a library project containing reusable code to call su) and [libsuperuser_example @ GitHub] (an example project using that library, and demonstrating some techniques to call su in the background).
The goal of these two projects is specifically not to provide a perfect library catering to your every root need. The goal is to demonstrate techniques you may want to reuse in your root app that work around common problems, in as little code as possible. Advanced techniques like for example maintaining a background su session and using that session when needed are not covered, for the sake of keeping it simple. The code is short, I would advise you to simply read the source for both projects.
Please note that this library behaves slightly differently in debug mode (unsigned APK), providing additional logging and exceptions. Some versions of the ADT do not set/unset debug mode correctly when signing and exporting the final APK. You should definitely check that your exported APKs are not still logging all shell calls before publishing them!
[Development Note] The library and sample code has been brought together rather quickly from different code in different projects, and is still being reviewed. If there are obvious bugs please let me know.

3. How to call su


3.1. Common pitfalls


Runtime.exec() and ProcessBuilder
It is tempting to use Runtime.getRuntime().exec("su -c [command]");, but you should be aware that [command]should be a single parameter, and thus may require quoting. Unfortunately both quoting the [command] parameter as well as passing the paramaters as separate variables to either Runtime.exec() or ProcessBuilder does not work consistently across all Android versions, and thus this construct should be avoided entirely. It is not impossible to do this right - but there's a high risk of problems.
Outputting a script for su to run
As one workaround to the above, various root app authors have taken to writing scripts and then calling su -c /path/to/script to execute all the commands. This does avoid potential parameter passing issues, but you are creating an unneeded temporary file and requires that your writable path does not contain a space. And while the latter is true for the moment, it is a bad idea to depend on that.
Rapid successive su calls
The su call is an expensive operation, and usually involves quite a bit of code being executed, as well as I/O being performed. It is good practise as well as good for performance to batch your commands together as much as possible. Launch as few su processes as you can, and perform as many commands per process as possible.
Many su calls throughout the app's lifecycle
Some apps need to make a lot of su calls throughout the app's lifecycle but can't batch these commands together. In such a case you should consider starting an interactive su shell and keeping it alive alongside your app, so you can pipe commands to it as needed. This might have positive effects on the performance and responsiveness of your application.
Hardcoded checks
The rights management application is not always /system/app/Superuser.apk. The package name is not a constant either. The su binary's location is not always /system/xbin/su. Many apps have hardcoded checks like these to find su. This is a bad idea and completely unreliable.
Assuming the su binary accepts parameters
Not all su binaries support all parameters. Worse, there's a good chance the su binary will start an interactive shell instead of producing an error if an unknown parameter is present (the -v parameter to check version is a good example of this). If you do not anticipate this, your process might never regain control from the su call, and could become stuck.

3.2. Making the call

A common method to call su that avoids the known issues listed above is by creating an interactive shell and piping commands to it. This is done by calling Runtime.getRuntime().exec("su");, and retrieving input and output streams from the returned Process object. Doing this is a fairly straight-forward piece of code, but including the debug logs and checks it's a bit long to reproduce here.
The core code is located here: [libsuperuser :: Shell.java @ GitHub]Shell.run() is a generic call to run shell code, the following more specific (static) utility functions are the ones you will probably end up using:
 List<String> Shell.SH.run(String command)
 List<String> Shell.SH.run(List<String> commands)
 List<String> Shell.SH.run(String[] commands)

 List<String> Shell.SU.run(String command)
 List<String> Shell.SU.run(List<String> commands)
 List<String> Shell.SU.run(String[] commands)
The SH variants are used for a non-root shell, where the SU variants are used for a root shell. These calls return a List<String> containing the output of the shell commands. If there was no output, the list is empty, but not null. The result is only null in case an error occured - including the user not granting your app su access. These are blockingcalls.
Note that in debug compiles, all shell STDIN/STDOUT/STDERR will be logged to logcat, and these calls will (intentionally) crash your app if called from the main thread. The reason for this will be discussed in section 4. When to call su.

3.3. Checking for su availability

There are many ways to check whether or not superuser access is available. The two most popular methods are either trying to detect the existance of the su binary or superuser package, or actually trying to run su and seeing what happens. I prefer the latter method, as running su is what you're after no matter where it's at and if you know how to find it - as long as the system does.
As further test, run the id command, which prints the ids of the current user and group, so you can use the output to confirm whether or not the shell you have started also really has root rights (the output contains uid=0).
A problem with the id command is that it depends on an external binary that must be present. I have never run into the situation where it wasn't, but to be sure we also issue an echo command that we check for, so that if the idcommand is not available, we can still know whether a shell was run at all. In the latter case, we assume the shell also has root priviliges - if this ends up being wrong, the user has worse problems than your app not getting root access. If you want to be absolutely sure even in this remote case of a wrongly rooted device, you would have to include its own native binary to perform the check. Of course, if you are including native binaries anyways, it is certainly advised to have them check if they are actually running as root before doing anything else.
[libsuperuser :: Shell.java @ GitHub] provides the following (static) utility function that performs this test for you:
 boolean Shell.SU.available()
This is a blocking call.

3.4. Checking for su version

While this is not something most root apps need to do, and not all su binaries even support this, this might be something you want to do. Both (recent) Superuser su binaries as well as SuperSU su binaries support the -v (for display) and -V (for internal comparison) parameters to check the version number.
As stated above, a potential problem is that a su binary that doesn't support these parameters may start an interactive shell instead. A work-around for this is to pipe "exit\n" to the su process, this will assure your app gets control back from su.
[libsuperuser :: Shell.java @ GitHub] provides the following (static) utility function that retrieves these version numbers (for the su binary, not the GUI package), or returns null if unable:
 String Shell.SU.version(boolean internal)
This is a blocking call.

4. When to call su


4.1. When not to call su

The main thread
Do not call su when running on the main application thread. The number one reason for freezes and crashes when apps request root access is that su is being called from the main thread. You should consider the su command to be equivalent to a blocking I/O call, like disk or network access. These should not be done from the main thread either - in fact, on newer Android versions, performing network I/O in the main thread will (intentionally) crash your app, and in strict mode the screen will flash red if you perform any disk I/O on the main thread to warn you of your error.
Blocking I/O calls on the main thread are bad because it is completely dependant on external factors how long the call will take. The call may take 100 milliseconds, 30 seconds, or an hour. Even if you think some minor I/O command should never take more than a few milliseconds, you still shouldn't do it from the main thread - you're making a guarantee you can't deliver on, and probably reducing the responsiveness of your app. If the main application threadblocks like this for more than a few seconds, an Application Not Responding (ANR) crash is generated by the system.
Because the su call is pretty much guaranteed to perform blocking I/O itself, and might even need to show its GUI and wait for user input, you cannot make any assumptions about how long it will take for the call to return, and thus you should never call it from the main thread.
I have often received complaints from SuperSU users about the countdown timer in the root access request popup that is (at the time of this writing) enabled by default, and will automatically deny root access after a number of seconds. The only reason this timer was built is because far too many root apps call su from the main thread, and if the popup would not time out and the user wouldn't grant or deny root access relatively quickly, the app that had requested su access would crash with an ANR. The timer helps reduce (but not eliminate) the chances of that happening, but it is merely treating symptoms, not solving problems.
A note on su being a blocking call
Most ways to start a new process are actually non-blocking, and the child process runs in a different thread. However, most example and library code either call Process.waitFor or read/write from/to the process's STDIN, STDOUT or STDERR stream. All of these possibilities turn the code that calls su into blocking code, waiting for the su process.

It is actually possible to call su in a non-blocking fashion from the main thread, but not with the example code accompanying this article. It is easy to make mistakes that way, and it is often simpler to create a single block of code that handles calling su (like the sample Shell.XX.run code provided) and simply run that from a different thread.

There are however situations where calling su in a non-blocking way is actually preferred (like keeping a su session open in the background and continuously reading and writing from/to it), but these are not covered by this article or the sample code.
BroadcastReceivers
Most of the time BroadcastReceivers run on the main thread (something often overlooked), and thus no blocking I/Ocalls like su should be made. Aside from running on the main thread, the su call itself may need to broadcast an intent to communicate with the GUI. This presents a problem because the latter broadcast may be waiting on the previous broadcast to complete, which it will not do until the onReceive method completes, which is in turn waiting for the su call to complete. This will cause an ANR.
Services
As with BroadcastReceivers, many developers at first overlook that a basic Service also executes on the main thread, and is thus also susceptible to ANRs. That is, unless you are using a special Service subclass (like IntentService) that uses a background thread, or added a background thread to the service yourself.

4.2. Detecting the main thread

Detecting if your code is running on the main thread is generally done as follows:
 if (Looper.myLooper() == Looper.getMainLooper()) {
  // running on the main thread
 } else {
  // not running on the main thread
 }
You might have noticed this code in the Shell.run() call in [libsuperuser :: Shell.java @ GitHub]. If it is detected you are running on the main thread, and the Android project is compiled in debug mode (BuildConfig.DEBUG == true), an exception will be thrown and your application will crash. Hopefully this will convince you to try and run your shell code in a background thread - and not to remove the check!

4.3. Using AsyncTask

The AsyncTask class is often used to perform quick and easy background processing for relatively short operations. You can safely call su from the AsyncTask's doInBackground method. Here is an example of a minimal implementation inside an Activity:
public class MainActivity extends Activity {
 private class Startup extends AsyncTask<Void, Void, Void> {
  @Override
  protected Void doInBackground(Void... params) {
   // this method is executed in a background thread
   // no problem calling su here

   return null;
  }
 }

 @Override
 public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);

  // start background task
  (new Startup()).execute();
 }
}
Of course, usually you may want to execute some code before and after the background task runs, like showing and hiding a ProgressDialog so the user knows a background action is being performed, and the GUI can't be used until that action completes: [libsuperuser_example :: MainActivity.java @ GitHub] contains a basic example.

4.4. Using IntentService

While AsyncTask is a very useful class you will no doubt often employ, sometimes it's just not the right tool for the job. For example, you can't directly use an AsyncTask from a BroadcastReceiver, because once the onReceive method completes, your process may not have any active components left, and thus may be killed. The correct thing to do for any blocking I/O - including su calls - from a BroadcastReceiver is starting a Service and running the code from there.
However, a standard Service actually runs on the main thread as well, unless you do the extra work to run code in a background thread. The IntentService class however, is an easy to use Service subclass designed specifically to run tasks (expressed by Intents) in a background thread, and automatically stop itself when it runs out of work. Perfect for fire-and-forget style tasks.
Many apps use a BOOT_COMPLETED BroadcastReceiver to perform some processing after the device is booted, without user interaction - a perfect case for IntentService. Your AndroidManifest.xml file will start out looking something like this, with a public (exported) BroadcastReceiver and a private (non-exported) IntentService:
 <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

 <application ...>
  ...

  <receiver android:name=".BootCompleteReceiver">
   <intent-filter>
    <action android:name="android.intent.action.BOOT_COMPLETED" />
    <category android:name="android.intent.category.HOME" />
   </intent-filter>
  </receiver>

  <service android:name=".BackgroundIntentService" />

  ...
 </application>
Next, we create a very basic IntentService in BackgroundIntentService.java:
public class BackgroundIntentService extends IntentService {
 public static void launchService(Context context) {
  if (context == null) return;
  context.startService(new Intent(context, BackgroundIntentService.class));
 }

 public BackgroundIntentService() {
  super("BackgroundIntentService");
 }

 @Override
 protected void onHandleIntent(Intent intent) {
  // the code you put here will be executed in a background thread
 }
}
All that is left now, is to launch this background service from your BroadcastReceiver, in BootCompleteReceiver.java:
public class BootCompleteReceiver extends BroadcastReceiver{
 @Override
 public void onReceive(Context context, Intent intent) {
  BackgroundIntentService.launchService(context);
 }
}
That is all there is to it - once you know how, it's incredibly easy. Of course, this IntentService only performs a single action and doesn't take any parameters: [libsuperuser_example :: BackgroundIntentService.java @ GitHub] contains a more elaborate example.
Instead of running in your BroadcastReceiver on the main thread, your code is now running safely in a background thread without risking an ANR crash. There is however a minor snag. Many apps send Toasts from theirBroadcastReceivers - this is a bit harder to do from a background thread due to some minor bugs in the Android framework. Refer to [libsuperuser :: Application.java @ GitHub] for a workaround.

5. Updates


5.1. Gobbling

On December 17 2012, [libsuperuser @ GitHub] has been updated with Gobblers to consume STDOUT and STDERR. These are nothing more than background threads that consume STDOUT and STDERR output as fast as possible. The exact how and why is a long story (if interested, read [When Runtime.exec() won't @ JavaWorld]), but this avoids potential deadlocks when excess output occurs on STDOUT or STDERR. If you are using my library please make sure you are running the latest version. If you're not running my library it may be wise to read the linked article and see if there is a problem with your code.

5.2. Full content logging

SuperSU has a feature to log all su command content. While this works fine for most apps, some apps can run into unexpected problems when this feature is used. One example is terminal emulators - these will not show the command prompt if a su shell is started. SuperSU v0.97 (released November 29 2012) and newer support a way to let SuperSU know your app does not function well with full content logging enabled. If you use this method, SuperSU will not enable full content logging for your app if SuperSU has only been configured to log by default. If the user goes into app-specific configuration, the user can still enable full content logging for your app manually. The user will in that case be presented with a warning.
To let SuperSU know your app is not fully compatible with full content logging, add the following to an Activity,Service, or BroadcastReceiver:
 <meta-data android:name="eu.chainfire.supersu.meta.contentlogging.problematic" android:value="true" />
For example:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" ...>
 <application ...>
  <activity ...>
   <intent-filter>
    ...
   </intent-filter>
   <meta-data android:name="eu.chainfire.supersu.meta.contentlogging.problematic" android:value="true" />
  </activity>
 </application>
</manifest>
Adding it to a single ActivityService, or BroadcastReceiver, is enough to get the entire package excluded from full content logging. There is no need to add it multiple times.
Please note that I will not tolerate abuse of this feature. Full content logging is there for the end-user, and it should not be disabled this way without good reason. I may resort to blacklisting your package from root access altogether if you purposely abuse this.

5.3. Parameters for su

SuperSU originally took its parameter parsing from ChainsDD's Superuser's su binary. On January 11 2012 modifications regarding parameter parsing were pushed to ChainsDD's GitHub [fc7479fab2 @ GitHub]. SuperSU has virtually identical updated parameter parsing from v1.00. While this does allow for some interesting constructs in calling su, you must be aware that not all constructs possible with the original parameter parsing will be interpreted in the same way with the new parameter parsing.
I would also like to point out specifically that (1) the command to execute, following the -c or --command parameter, should be a single parameter, (2) that parameter is not even supported by all su variants available in the wild, and (3) the most reliable way to execute commands as root still remains starting su as a shell and piping commands and output.
Some su variants on some devices do not support anything else than being started as an interactive shell. Exact parameter parsing of the more functional su binaries differs by author and by version, sometimes very subtly. The older the version of Android your app can run on, the higher the chance of running into an exotic or incompatible subinary. You'd be surprised what your app can run into in the wild.
As such, in my personal opinion, it is always wisest and most compatible to simply run su as an interactive shell and pipe commands and output. If you must deviate from this, you should at least thoroughly test your app with (1) the most recent Superuser, (2) a Superuser (and binary) from 2011, (3) SuperSU v0.99 or older, and (4) SuperSU v1.00 or newer.

5.4. ACCESS_SUPERUSER permission

From SuperSU version 1.20 and onwards, the android.permission.ACCESS_SUPERUSER permission is declared by SuperSU. All root apps should from now on declare this permission in their AndroidManifest.xml:
 <uses-permission android:name="android.permission.ACCESS_SUPERUSER" />
If this permission is not present, SuperSU will present a warning in its superuser request popup (this is configurable in SuperSU settings). At the time of this writing this permission is not enforced, but it is expected that sometime in the future it will be, and apps requesting root that do not have this permission set will be silently denied.
If this permission is declared, the user will be able to see in the app permissions list that the app requests superuser access.
--- EOF ---

沒有留言:

張貼留言