Abrarul8541 commited on
Commit
47e70c0
·
verified ·
1 Parent(s): 748696e

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/sh").exists() || File(rootfs, "bin/busybox").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,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
- /** Runs [cmd] with /bin/sh inside the rootfs and streams combined output line by line. */
40
- fun exec(cmd: String, onLine: (String) -> Unit): Int {
 
 
 
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 pb = ProcessBuilder(
45
- bin.path, "-0", "-r", rootfs.path,
46
- "-b", "/dev", "-b", "/proc", "-b", "/sys",
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
- ).redirectErrorStream(true)
 
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 } // path traversal guard
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)