v2.2.4: Bundle proot as native library - bypasses SELinux execute restriction
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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:!*~'
|
||||||
|
|||||||
@@ -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");
|
|
||||||
if (proot.exists()) {
|
|
||||||
try { Os.chmod(proot.getAbsolutePath(), 0755); } catch (Exception e) {}
|
|
||||||
prootPath = proot.getAbsolutePath();
|
|
||||||
} else {
|
|
||||||
String nativeLibDir = getNativeLibDir();
|
String nativeLibDir = getNativeLibDir();
|
||||||
if (nativeLibDir != null) {
|
|
||||||
File nativeProot = new File(nativeLibDir, "libproot.so");
|
File prootInPrefix = new File(prefixDir + "/bin/proot");
|
||||||
if (nativeProot.exists()) {
|
if (!prootInPrefix.exists() && nativeLibDir != null) {
|
||||||
prootPath = nativeProot.getAbsolutePath();
|
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");
|
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());
|
||||||
|
|||||||
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",
|
"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": {
|
||||||
|
|||||||
@@ -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 & GLM-5.1</p>
|
<p class="about-text">Built with Z.AI SDK & 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>
|
||||||
|
|||||||
@@ -1990,22 +1990,26 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function tryProotInstall(prefixUsr, pkgBin, aptBin) {
|
async function tryProotInstall(prefixUsr, pkgBin, aptBin) {
|
||||||
|
var prootCmd = termState.prootPath;
|
||||||
|
if (!prootCmd) {
|
||||||
try {
|
try {
|
||||||
termPrint('[*] Downloading PRoot from Termux repo...', 'info');
|
|
||||||
var prootResult = await Bootstrap.installProot();
|
var prootResult = await Bootstrap.installProot();
|
||||||
if (!prootResult || !prootResult.path) {
|
if (prootResult && prootResult.path) {
|
||||||
termPrint('[!] PRoot download failed', 'err');
|
prootCmd = prootResult.path;
|
||||||
return false;
|
termState.prootPath = prootCmd;
|
||||||
}
|
|
||||||
termPrint('[OK] PRoot downloaded: ' + prootResult.path, 'success');
|
|
||||||
termState.hasProot = true;
|
termState.hasProot = true;
|
||||||
termState.prootPath = prootResult.path;
|
}
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
termPrint('[!] PRoot download failed: ' + e.message, 'err');
|
termPrint('[!] PRoot download failed: ' + e.message, 'err');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!prootCmd) {
|
||||||
|
termPrint('[!] No PRoot available', '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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user