Proot: bind mounts for plugin sandbox, safer exec
Browse files
app/src/main/java/io/github/abrarul/modularai/proot/Proot.kt
CHANGED
|
@@ -22,7 +22,7 @@ class Proot(private val ctx: Context) {
|
|
| 22 |
val rootfs = File(ctx.filesDir, "rootfs/alpine")
|
| 23 |
|
| 24 |
val binaryAvailable get() = bin.exists() && loader.exists()
|
| 25 |
-
val installed get() = File(rootfs, "bin/
|
| 26 |
|
| 27 |
suspend fun installAlpine(onLog: (String) -> Unit) = withContext(Dispatchers.IO) {
|
| 28 |
val url = "https://dl-cdn.alpinelinux.org/alpine/v3.20/releases/aarch64/alpine-minirootfs-3.20.3-aarch64.tar.gz"
|
|
@@ -32,23 +32,28 @@ class Proot(private val ctx: Context) {
|
|
| 32 |
URL(url).openStream().use { Tar.extract(it, rootfs) { n++ } }
|
| 33 |
File(rootfs, "etc/resolv.conf").apply { parentFile?.mkdirs(); writeText("nameserver 8.8.8.8\nnameserver 1.1.1.1\n") }
|
| 34 |
onLog("Extracted $n entries into ${rootfs.path}")
|
|
|
|
| 35 |
}
|
| 36 |
|
| 37 |
fun remove() = rootfs.deleteRecursively()
|
| 38 |
|
| 39 |
-
/**
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
| 41 |
check(binaryAvailable) { "proot binary not bundled in this build (use 'termux ...' instead)" }
|
| 42 |
check(installed) { "Rootfs not installed. Run: proot install" }
|
| 43 |
val tmp = File(ctx.filesDir, "tmp").apply { mkdirs() }
|
| 44 |
-
val
|
| 45 |
-
|
| 46 |
-
|
| 47 |
"-w", "/root",
|
| 48 |
"/usr/bin/env", "-i", "HOME=/root", "TERM=xterm-256color",
|
| 49 |
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
| 50 |
"/bin/sh", "-c", cmd
|
| 51 |
-
)
|
|
|
|
| 52 |
pb.environment()["PROOT_LOADER"] = loader.path
|
| 53 |
pb.environment()["PROOT_TMP_DIR"] = tmp.path
|
| 54 |
val p = pb.start()
|
|
@@ -97,7 +102,7 @@ object Tar {
|
|
| 97 |
longName = null; paxPath = null; paxLink = null
|
| 98 |
|
| 99 |
val out = File(dest, name.trimStart('/')).let { File(it.parentFile?.canonicalFile ?: dest, it.name) }
|
| 100 |
-
if (!out.path.startsWith(root)) { skip(inp, size + pad); continue } //
|
| 101 |
onEntry()
|
| 102 |
|
| 103 |
when (type) {
|
|
@@ -109,6 +114,7 @@ object Tar {
|
|
| 109 |
}
|
| 110 |
'0', '\u0000' -> {
|
| 111 |
out.parentFile?.mkdirs()
|
|
|
|
| 112 |
out.outputStream().use { o ->
|
| 113 |
var left = size
|
| 114 |
val buf = ByteArray(1 shl 16)
|
|
|
|
| 22 |
val rootfs = File(ctx.filesDir, "rootfs/alpine")
|
| 23 |
|
| 24 |
val binaryAvailable get() = bin.exists() && loader.exists()
|
| 25 |
+
val installed get() = File(rootfs, "bin/busybox").exists() || File(rootfs, "bin/sh").exists()
|
| 26 |
|
| 27 |
suspend fun installAlpine(onLog: (String) -> Unit) = withContext(Dispatchers.IO) {
|
| 28 |
val url = "https://dl-cdn.alpinelinux.org/alpine/v3.20/releases/aarch64/alpine-minirootfs-3.20.3-aarch64.tar.gz"
|
|
|
|
| 32 |
URL(url).openStream().use { Tar.extract(it, rootfs) { n++ } }
|
| 33 |
File(rootfs, "etc/resolv.conf").apply { parentFile?.mkdirs(); writeText("nameserver 8.8.8.8\nnameserver 1.1.1.1\n") }
|
| 34 |
onLog("Extracted $n entries into ${rootfs.path}")
|
| 35 |
+
if (!binaryAvailable) onLog("Note: libproot.so is not bundled in this build, so commands cannot run yet (see jniLibs/README.md).")
|
| 36 |
}
|
| 37 |
|
| 38 |
fun remove() = rootfs.deleteRecursively()
|
| 39 |
|
| 40 |
+
/**
|
| 41 |
+
* Runs [cmd] with /bin/sh inside the rootfs and streams combined output line by line.
|
| 42 |
+
* [binds] are "hostPath:guestPath" pairs. Nothing else of the app's private data is visible inside.
|
| 43 |
+
*/
|
| 44 |
+
fun exec(cmd: String, onLine: (String) -> Unit, binds: List<String> = emptyList()): Int {
|
| 45 |
check(binaryAvailable) { "proot binary not bundled in this build (use 'termux ...' instead)" }
|
| 46 |
check(installed) { "Rootfs not installed. Run: proot install" }
|
| 47 |
val tmp = File(ctx.filesDir, "tmp").apply { mkdirs() }
|
| 48 |
+
val args = mutableListOf(bin.path, "-0", "-r", rootfs.path, "-b", "/dev", "-b", "/proc", "-b", "/sys")
|
| 49 |
+
binds.forEach { args += listOf("-b", it) }
|
| 50 |
+
args += listOf(
|
| 51 |
"-w", "/root",
|
| 52 |
"/usr/bin/env", "-i", "HOME=/root", "TERM=xterm-256color",
|
| 53 |
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
| 54 |
"/bin/sh", "-c", cmd
|
| 55 |
+
)
|
| 56 |
+
val pb = ProcessBuilder(args).redirectErrorStream(true)
|
| 57 |
pb.environment()["PROOT_LOADER"] = loader.path
|
| 58 |
pb.environment()["PROOT_TMP_DIR"] = tmp.path
|
| 59 |
val p = pb.start()
|
|
|
|
| 102 |
longName = null; paxPath = null; paxLink = null
|
| 103 |
|
| 104 |
val out = File(dest, name.trimStart('/')).let { File(it.parentFile?.canonicalFile ?: dest, it.name) }
|
| 105 |
+
if (out.path != root && !out.path.startsWith(root + File.separator)) { skip(inp, size + pad); continue } // traversal guard
|
| 106 |
onEntry()
|
| 107 |
|
| 108 |
when (type) {
|
|
|
|
| 114 |
}
|
| 115 |
'0', '\u0000' -> {
|
| 116 |
out.parentFile?.mkdirs()
|
| 117 |
+
out.delete()
|
| 118 |
out.outputStream().use { o ->
|
| 119 |
var left = size
|
| 120 |
val buf = ByteArray(1 shl 16)
|