v2.2.4: Bundle proot as native library - bypasses SELinux execute restriction

This commit is contained in:
admin
2026-05-19 20:52:33 +04:00
Unverified
parent b921102768
commit ce0cf20eaf
16 changed files with 91 additions and 26 deletions

View File

@@ -631,6 +631,13 @@ data: [DONE]
## Changelog ## Changelog
### v2.2.4 (2026-05-19)
- **Bundled PRoot** — proot binary (v5.1.107) included in APK as native library with executable SELinux label
- **Auto-Proot Install** — `ShellPlugin.refreshShell()` copies proot + loader from APK's `nativeLibraryDir` to `$PREFIX/bin/` and `$PREFIX/libexec/proot/` on first use
- **Auto-Proot Wrapping** — commands using `$PREFIX/bin/` paths (pkg/apt/dpkg) automatically wrapped with `proot -0 -b /dev -b /proc -b /sys -r $PREFIX`
- **PROOT_LOADER env** — `PROOT_LOADER` environment variable set so proot finds its loader binary
- Supports arm64-v8a, armeabi-v7a, x86_64, x86 architectures (~350KB total added to APK)
### v2.2.3 (2026-05-19) ### v2.2.3 (2026-05-19)
- **Os.chmod() Fix** — uses `android.system.Os.chmod()` (direct syscall) instead of `Runtime.exec("chmod")` for reliable execute permissions - **Os.chmod() Fix** — uses `android.system.Os.chmod()` (direct syscall) instead of `Runtime.exec("chmod")` for reliable execute permissions
- **PRoot Fallback** — auto-downloads PRoot from Termux package repo when SELinux blocks binary execution - **PRoot Fallback** — auto-downloads PRoot from Termux package repo when SELinux blocks binary execution

View File

@@ -7,8 +7,8 @@ android {
applicationId "ai.z.chat" applicationId "ai.z.chat"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 16 versionCode 17
versionName "2.2.3" versionName "2.2.4"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions { aaptOptions {
ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~' ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'

View File

@@ -49,20 +49,41 @@ public class ShellPlugin extends Plugin {
} }
private String prootPath = null; private String prootPath = null;
private String prootLoaderPath = null;
private void refreshShell() { private void refreshShell() {
File proot = new File(prefixDir + "/bin/proot"); String nativeLibDir = getNativeLibDir();
if (proot.exists()) {
try { Os.chmod(proot.getAbsolutePath(), 0755); } catch (Exception e) {} File prootInPrefix = new File(prefixDir + "/bin/proot");
prootPath = proot.getAbsolutePath(); if (!prootInPrefix.exists() && nativeLibDir != null) {
} else { File bundledProot = new File(nativeLibDir, "libproot.so");
String nativeLibDir = getNativeLibDir(); File bundledLoader = new File(nativeLibDir, "libproot-loader.so");
if (nativeLibDir != null) { if (bundledProot.exists()) {
File nativeProot = new File(nativeLibDir, "libproot.so"); try {
if (nativeProot.exists()) { new File(prefixDir + "/bin").mkdirs();
prootPath = nativeProot.getAbsolutePath(); new File(prefixDir + "/libexec").mkdirs();
new File(prefixDir + "/libexec/proot").mkdirs();
copyFile(bundledProot, new File(prefixDir + "/bin/proot"));
Os.chmod(prefixDir + "/bin/proot", 0755);
if (bundledLoader.exists()) {
copyFile(bundledLoader, new File(prefixDir + "/libexec/proot/loader"));
Os.chmod(prefixDir + "/libexec/proot/loader", 0755);
File loader32 = new File(nativeLibDir, "libproot-loader32.so");
if (loader32.exists()) {
copyFile(loader32, new File(prefixDir + "/libexec/proot/loader32"));
Os.chmod(prefixDir + "/libexec/proot/loader32", 0755);
}
}
prootPath = prefixDir + "/bin/proot";
prootLoaderPath = prefixDir + "/libexec/proot/loader";
Log.i(TAG, "Installed bundled proot from APK: " + prootPath);
} catch (Exception e) {
Log.w(TAG, "Failed to install bundled proot: " + e.getMessage());
} }
} }
} else if (prootInPrefix.exists()) {
try { Os.chmod(prootInPrefix.getAbsolutePath(), 0755); } catch (Exception e) {}
prootPath = prootInPrefix.getAbsolutePath();
} }
File bash = new File(prefixDir + "/bin/bash"); File bash = new File(prefixDir + "/bin/bash");
@@ -77,6 +98,16 @@ public class ShellPlugin extends Plugin {
shellPath = "/system/bin/sh"; shellPath = "/system/bin/sh";
} }
private void copyFile(File src, File dst) throws Exception {
java.io.FileInputStream fis = new java.io.FileInputStream(src);
java.io.FileOutputStream fos = new java.io.FileOutputStream(dst);
byte[] buf = new byte[8192];
int r;
while ((r = fis.read(buf)) > 0) fos.write(buf, 0, r);
fos.close();
fis.close();
}
private String getNativeLibDir() { private String getNativeLibDir() {
try { try {
return getContext().getApplicationInfo().nativeLibraryDir; return getContext().getApplicationInfo().nativeLibraryDir;
@@ -106,6 +137,8 @@ public class ShellPlugin extends Plugin {
if (useProot && prootPath != null) { if (useProot && prootPath != null) {
actualCommand = prootPath + " -0 -b /dev -b /proc -b /sys -r " + prefixDir + " /bin/sh -c " + bashEscape(command); actualCommand = prootPath + " -0 -b /dev -b /proc -b /sys -r " + prefixDir + " /bin/sh -c " + bashEscape(command);
} else if (prootPath != null && isTermuxCommand(command)) {
actualCommand = prootPath + " -0 -b /dev -b /proc -b /sys -r " + prefixDir + " /bin/sh -c " + bashEscape(command);
} }
ProcessBuilder pb = new ProcessBuilder(shell, "-c", actualCommand); ProcessBuilder pb = new ProcessBuilder(shell, "-c", actualCommand);
@@ -141,6 +174,11 @@ public class ShellPlugin extends Plugin {
return "'" + s.replace("'", "'\\''") + "'"; return "'" + s.replace("'", "'\\''") + "'";
} }
private boolean isTermuxCommand(String cmd) {
if (prefixDir == null) return false;
return cmd.contains(prefixDir + "/bin/") || cmd.contains("pkg ") || cmd.contains("apt ") || cmd.contains("dpkg ");
}
@PluginMethod @PluginMethod
public void kill(PluginCall call) { public void kill(PluginCall call) {
String processId = call.getString("pid", ""); String processId = call.getString("pid", "");
@@ -334,6 +372,9 @@ public class ShellPlugin extends Plugin {
if (hasOurPrefix) { if (hasOurPrefix) {
envList.add("LD_LIBRARY_PATH=" + prefixDir + "/lib"); envList.add("LD_LIBRARY_PATH=" + prefixDir + "/lib");
envList.add("BOOTSTRAP=zaichat"); envList.add("BOOTSTRAP=zaichat");
if (prootLoaderPath != null) {
envList.add("PROOT_LOADER=" + prootLoaderPath);
}
} }
if (hasTermux) { if (hasTermux) {
envList.add("TERMUX_VERSION=" + getTermuxVersion()); envList.add("TERMUX_VERSION=" + getTermuxVersion());

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,6 +1,6 @@
{ {
"name": "zai-chat", "name": "zai-chat",
"version": "2.2.3", "version": "2.2.4",
"description": "Z.AI Chat - Full stack AI chat powered by GLM Coding Plan", "description": "Z.AI Chat - Full stack AI chat powered by GLM Coding Plan",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {

View File

@@ -327,13 +327,24 @@
</div> </div>
<div class="settings-section"> <div class="settings-section">
<h3>About</h3> <h3>About</h3>
<p class="about-text">Z.AI Chat v2.2.3</p> <p class="about-text">Z.AI Chat v2.2.4</p>
<p class="about-text">Built with Z.AI SDK &amp; GLM-5.1</p> <p class="about-text">Built with Z.AI SDK &amp; GLM-5.1</p>
<p class="about-text">Compatible with Android 15/16</p> <p class="about-text">Compatible with Android 15/16</p>
</div> </div>
<div class="settings-section"> <div class="settings-section">
<h3>Changelog</h3> <h3>Changelog</h3>
<ul class="changelog-list"> <ul class="changelog-list">
<li>
<span class="changelog-version">v2.2.4</span>
<span class="changelog-date">2026-05-19</span>
<ul>
<li><strong>Bundled PRoot</strong> — proot binary included in APK as native library (executable SELinux label)</li>
<li><strong>Auto-Proot Install</strong> — copies proot + loader from APK to Termux prefix on first shell use</li>
<li><strong>Auto-Proot Wrapping</strong> — pkg/apt/dpkg commands automatically wrapped with proot</li>
<li><strong>PROOT_LOADER env</strong> — loader path set correctly for proot execution</li>
<li>Supports arm64-v8a, armeabi-v7a, x86_64, x86 architectures</li>
</ul>
</li>
<li> <li>
<span class="changelog-version">v2.2.3</span> <span class="changelog-version">v2.2.3</span>
<span class="changelog-date">2026-05-19</span> <span class="changelog-date">2026-05-19</span>

View File

@@ -1990,22 +1990,26 @@
} }
async function tryProotInstall(prefixUsr, pkgBin, aptBin) { async function tryProotInstall(prefixUsr, pkgBin, aptBin) {
try { var prootCmd = termState.prootPath;
termPrint('[*] Downloading PRoot from Termux repo...', 'info'); if (!prootCmd) {
var prootResult = await Bootstrap.installProot(); try {
if (!prootResult || !prootResult.path) { var prootResult = await Bootstrap.installProot();
termPrint('[!] PRoot download failed', 'err'); if (prootResult && prootResult.path) {
return false; prootCmd = prootResult.path;
termState.prootPath = prootCmd;
termState.hasProot = true;
}
} catch(e) {
termPrint('[!] PRoot download failed: ' + e.message, 'err');
} }
termPrint('[OK] PRoot downloaded: ' + prootResult.path, 'success'); }
termState.hasProot = true;
termState.prootPath = prootResult.path; if (!prootCmd) {
} catch(e) { termPrint('[!] No PRoot available', 'err');
termPrint('[!] PRoot download failed: ' + e.message, 'err');
return false; return false;
} }
var prootCmd = termState.prootPath; termPrint('[OK] PRoot available: ' + prootCmd, 'success');
var pkgCmd = 'sh "' + pkgBin + '" update -y 2>&1 && sh "' + pkgBin + '" install -y aapt2 ecj dx apksigner 2>&1'; var pkgCmd = 'sh "' + pkgBin + '" update -y 2>&1 && sh "' + pkgBin + '" install -y aapt2 ecj dx apksigner 2>&1';
var wrappedCmd = prootCmd + ' -0 -b /dev -b /proc -b /sys -r ' + prefixUsr + ' /bin/sh -c \'' + pkgCmd.replace(/'/g, "'\\''") + '\''; var wrappedCmd = prootCmd + ' -0 -b /dev -b /proc -b /sys -r ' + prefixUsr + ' /bin/sh -c \'' + pkgCmd.replace(/'/g, "'\\''") + '\'';
@@ -2024,6 +2028,8 @@
termState.devToolsInstalled = true; termState.devToolsInstalled = true;
return true; return true;
} }
termPrint('[!] PRoot execution result: exit code ' + result.exitCode, 'err');
return false; return false;
} }