Xamarin and Java Processes (root)

|

I have seen this question pop up from time to time, where someone asks how to run a native process on Android. This is usually because they want to run something with root permissions, which could allow you to do various things, such as reading files you are usually not allowed. This could be reading VPN state from /data/misc/vpn/state. Blocking Advertisements by adding domains to /etc/hosts and much more.

This blog post will show you how to start a Java process and read the exit code, and the output from the process.

Note: If you want to try this yourself, you will need to have a rooted device. I like to run LineageOS on my devices. They support a large amount of popular devices. On LineageOS you can install SU by grabbing it from the Extras web page and flashing using your recovery. This is up to the reader to do.

Linux Processes

Linux is the operating system Android is based on. A standard process on Linux will use

  • stdin to read input from the console (i.e. keyboard)
  • stdout to write to the console
  • stderr to write errors to the console

These are in fact file handles to /proc/<pid>/fd/{0,1,2}, where <pid> is your process id and 0, 1, 2 are stdin, stdout, stderr respectively. You should not worry about this as this is usually handled by the OS.

A process has an exit code. 0 means everything was OK, anything else than 0 means it probably failed.

Starting a Process On Android

On Android you can start a native Process using with Java.Lang.Runtime.GetRuntime().Exec(<command>), which will return a Java.Lang.Process. Alternatively (also preferred way) if you need more control you can use Java.Lang.ProcessBuilder, which allows you to redirect stdin, stdout, stderr. Its Start() method, similarly to Exec() gives you a Java.Lang.Process. This object reflects the Linux process and has streams for stdin, stdout and stderr. These are our way into reading what the process writes out.

Xamarin has some nice helpers for us in the Java.Lang.Process they expose. Instead of blocking when we want to wait for the process to finish, they have added WaitForAsync(), which is very nice. Also the streams for stdin, stdout and stderr are nice C# Streams that you can read and write to very easily. Namely InputStream, OutputStream and ErrorStream.

var builder = new ProcessBuilder("echo", "hello");
var process = builder.Start();
var exitCode = await process.WaitForAsync();

The example above runs the echo command, which just echoes what follows after. In this case “hello”. The WaitForAsync() method returns us a Task<int> with the exit code of the process.

Keep in mind that WaitForAsync() also can throw exceptions such as IOException and SecurityException depending on what you are doing.

Reading From The Process

When we start a Process, it should be considered that we start a process and execute some commands that redirect into this Process. Meaning most of the time, when we want to read the result from the commands we run, we will actually need to read from InputStream, rather than OutputStream which would be the usual stdout. However, since our Process is the one starting other Processes, then their output stream gets redirected into our input stream.

Anyways, reading the stream is super easy.

using (var outputStreamReader = new StreamReader(process.InputStream))
{
    var output = await outputStreamReader.ReadToEndAsync();
}

Bingo! If we ran this on the echo hello process we created further up, output would be hello in this case. Neat!

Running a Process as Root

Running a process as root is super easy. Just add su and -c to the commands you execute as follows.

var builder = new ProcessBuilder("su", "-c", "echo hello");

-c means that the root process su (super user aka. root) should run a specific command. Notice that echo and hello became one argument. This is because we are redirecting into su’s stdin. Apart from that everything else stays the same.

Most Android OS’s with root, will show a dialog when root is requested.

Root dialog

Reading Large Files

Reading large files by using cat to our Process stdin might be inefficient. You could consider using the ProcessBuilder to redirect the stdin directly to a file at a destination you have better access to.

var myFile = new File("/some/path");
builder.RedirectInput(myFile);

Alternatively you could do the same with the InputStream and read it to a file.

using (var inputStream = process.InputStream)
using (var fileStream = File.Create("/some/path"))
{
    await inputStream.CopyToAsync(fileStream);
}

Convenience Helper Method

Here is a little helper method for running simple commands and getting their result.

async Task<(int exitCode, string result)> RunCommand(params string[] command)
{
    string result = null;
    var exitCode = -1;
    try
    {
        var builder = new ProcessBuilder(command);
        var process = builder.Start();
        exitCode = await process.WaitForAsync();

        if (exitCode == 0)
        {
            using (var inputStreamReader = new StreamReader(process.InputStream))
            {
                result = await inputStreamReader.ReadToEndAsync();
            }
        }
        else if (process.ErrorStream != null)
        {
            using (var errorStreamReader = new StreamReader(process.ErrorStream))
            {
                var error = await errorStreamReader.ReadToEndAsync();
                result = $"Error {error}";
            }
        }
    }
    catch (IOException ex)
    {
        result = $"Exception {ex.Message}";
    }

    return (exitCode, result);
}

Usage would be something like.

var (exitCode, result) = await RunCommand("su", "-c", "cat /data/misc/vpn/state");

This would return with the exitCode 0 if everything went well, and the string result would contain something like.

ppp0
10.0.0.42/32
0.0.0.0/0
109.226.17.2 144.117.5.51

Nice! Now you should be an expert in native processes and how to launch them from your Android App. It is up to you the reader as an exercise to figure out how OutputStream works. Enjoy!