v2.2.4: Bundle proot as native library - bypasses SELinux execute restriction
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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:!*~'
|
||||
|
||||
@@ -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();
|
||||
String nativeLibDir = getNativeLibDir();
|
||||
|
||||
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());
|
||||
|
||||
BIN
android/app/src/main/jniLibs/arm64-v8a/libproot-loader.so
Executable file
BIN
android/app/src/main/jniLibs/arm64-v8a/libproot-loader.so
Executable file
Binary file not shown.
BIN
android/app/src/main/jniLibs/arm64-v8a/libproot-loader32.so
Executable file
BIN
android/app/src/main/jniLibs/arm64-v8a/libproot-loader32.so
Executable file
Binary file not shown.
BIN
android/app/src/main/jniLibs/arm64-v8a/libproot.so
Executable file
BIN
android/app/src/main/jniLibs/arm64-v8a/libproot.so
Executable file
Binary file not shown.
BIN
android/app/src/main/jniLibs/armeabi-v7a/libproot-loader.so
Executable file
BIN
android/app/src/main/jniLibs/armeabi-v7a/libproot-loader.so
Executable file
Binary file not shown.
BIN
android/app/src/main/jniLibs/armeabi-v7a/libproot.so
Executable file
BIN
android/app/src/main/jniLibs/armeabi-v7a/libproot.so
Executable file
Binary file not shown.
BIN
android/app/src/main/jniLibs/x86/libproot-loader.so
Executable file
BIN
android/app/src/main/jniLibs/x86/libproot-loader.so
Executable file
Binary file not shown.
BIN
android/app/src/main/jniLibs/x86/libproot.so
Executable file
BIN
android/app/src/main/jniLibs/x86/libproot.so
Executable file
Binary file not shown.
BIN
android/app/src/main/jniLibs/x86_64/libproot-loader.so
Executable file
BIN
android/app/src/main/jniLibs/x86_64/libproot-loader.so
Executable file
Binary file not shown.
BIN
android/app/src/main/jniLibs/x86_64/libproot-loader32.so
Executable file
BIN
android/app/src/main/jniLibs/x86_64/libproot-loader32.so
Executable file
Binary file not shown.
BIN
android/app/src/main/jniLibs/x86_64/libproot.so
Executable file
BIN
android/app/src/main/jniLibs/x86_64/libproot.so
Executable file
Binary file not shown.
@@ -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": {
|
||||
|
||||
@@ -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 & 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>
|
||||
|
||||
@@ -1990,22 +1990,26 @@
|
||||
}
|
||||
|
||||
async function tryProotInstall(prefixUsr, pkgBin, aptBin) {
|
||||
try {
|
||||
termPrint('[*] Downloading PRoot from Termux repo...', 'info');
|
||||
var prootResult = await Bootstrap.installProot();
|
||||
if (!prootResult || !prootResult.path) {
|
||||
termPrint('[!] PRoot download failed', 'err');
|
||||
return false;
|
||||
var prootCmd = termState.prootPath;
|
||||
if (!prootCmd) {
|
||||
try {
|
||||
var prootResult = await Bootstrap.installProot();
|
||||
if (prootResult && prootResult.path) {
|
||||
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;
|
||||
} 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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user