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
### 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)
- **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

View File

@@ -7,8 +7,8 @@ android {
applicationId "ai.z.chat"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 16
versionName "2.2.3"
versionCode 17
versionName "2.2.4"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
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 prootLoaderPath = null;
private void refreshShell() {
File proot = new File(prefixDir + "/bin/proot");
if (proot.exists()) {
try { Os.chmod(proot.getAbsolutePath(), 0755); } catch (Exception e) {}
prootPath = proot.getAbsolutePath();
} else {
String nativeLibDir = getNativeLibDir();
if (nativeLibDir != null) {
File nativeProot = new File(nativeLibDir, "libproot.so");
if (nativeProot.exists()) {
prootPath = nativeProot.getAbsolutePath();
File prootInPrefix = new File(prefixDir + "/bin/proot");
if (!prootInPrefix.exists() && nativeLibDir != null) {
File bundledProot = new File(nativeLibDir, "libproot.so");
File bundledLoader = new File(nativeLibDir, "libproot-loader.so");
if (bundledProot.exists()) {
try {
new File(prefixDir + "/bin").mkdirs();
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");
@@ -77,6 +98,16 @@ public class ShellPlugin extends Plugin {
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() {
try {
return getContext().getApplicationInfo().nativeLibraryDir;
@@ -106,6 +137,8 @@ public class ShellPlugin extends Plugin {
if (useProot && prootPath != null) {
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);
@@ -141,6 +174,11 @@ public class ShellPlugin extends Plugin {
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
public void kill(PluginCall call) {
String processId = call.getString("pid", "");
@@ -334,6 +372,9 @@ public class ShellPlugin extends Plugin {
if (hasOurPrefix) {
envList.add("LD_LIBRARY_PATH=" + prefixDir + "/lib");
envList.add("BOOTSTRAP=zaichat");
if (prootLoaderPath != null) {
envList.add("PROOT_LOADER=" + prootLoaderPath);
}
}
if (hasTermux) {
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",
"version": "2.2.3",
"version": "2.2.4",
"description": "Z.AI Chat - Full stack AI chat powered by GLM Coding Plan",
"main": "index.js",
"scripts": {

View File

@@ -327,13 +327,24 @@
</div>
<div class="settings-section">
<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">Compatible with Android 15/16</p>
</div>
<div class="settings-section">
<h3>Changelog</h3>
<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>
<span class="changelog-version">v2.2.3</span>
<span class="changelog-date">2026-05-19</span>

View File

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