diff --git a/tests/tools/test_tirith_security.py b/tests/tools/test_tirith_security.py index 4229ae82c6b2..e31819d88a66 100644 --- a/tests/tools/test_tirith_security.py +++ b/tests/tools/test_tirith_security.py @@ -1151,6 +1151,77 @@ def _which_side_effect(name): _tirith_mod._resolved_path = None +# --------------------------------------------------------------------------- +# Termux native (Bionic libc) skip +# --------------------------------------------------------------------------- + +class TestTermuxNativeSkip: + """Issue #26275: tirith binary spawned on Termux native (no proot) fails + with `[Errno 2] No such file or directory` because the unknown-linux-gnu + binary embeds /lib/ld-linux-aarch64.so.1 as its dynamic interpreter, + which doesn't exist on Termux's Bionic-libc filesystem. The auto-installer + must skip this environment so we don't write a binary that can't run.""" + + def test_is_termux_native_true_when_prefix_set(self): + from tools.tirith_security import _is_termux_native + with patch.dict(os.environ, {"PREFIX": "/data/data/com.termux/files/usr"}): + assert _is_termux_native() is True + + def test_is_termux_native_false_when_prefix_unset(self): + from tools.tirith_security import _is_termux_native + env = {k: v for k, v in os.environ.items() if k != "PREFIX"} + with patch.dict(os.environ, env, clear=True): + assert _is_termux_native() is False + + def test_is_termux_native_false_for_unrelated_prefix(self): + """A non-Termux PREFIX (e.g. Conda's $PREFIX) must NOT match.""" + from tools.tirith_security import _is_termux_native + with patch.dict(os.environ, {"PREFIX": "/opt/conda/envs/dev"}): + assert _is_termux_native() is False + + def test_detect_target_returns_none_on_termux_native(self): + """Even on Linux/aarch64 (which would otherwise resolve to + aarch64-unknown-linux-gnu), Termux native must short-circuit to None + so the unsupported_platform path is taken in _install_tirith.""" + from tools.tirith_security import _detect_target + with patch.dict(os.environ, {"PREFIX": "/data/data/com.termux/files/usr"}), \ + patch("tools.tirith_security.platform.system", return_value="Linux"), \ + patch("tools.tirith_security.platform.machine", return_value="aarch64"): + assert _detect_target() is None + + def test_detect_target_returns_none_when_termux_reports_android(self): + """Some Python builds on Termux report platform.system() == 'Android' + instead of 'Linux'. The Termux PREFIX check must catch both.""" + from tools.tirith_security import _detect_target + with patch.dict(os.environ, {"PREFIX": "/data/data/com.termux/files/usr"}), \ + patch("tools.tirith_security.platform.system", return_value="Android"), \ + patch("tools.tirith_security.platform.machine", return_value="aarch64"): + assert _detect_target() is None + + def test_detect_target_proot_ubuntu_inside_termux_still_works(self): + """Inside a proot Ubuntu on Termux, PREFIX is rewritten away from + /data/data/com.termux/files/, so a real glibc is available and the + Linux-gnu binaries work normally. Must NOT return None.""" + from tools.tirith_security import _detect_target + with patch.dict(os.environ, {"PREFIX": "/usr"}), \ + patch("tools.tirith_security.platform.system", return_value="Linux"), \ + patch("tools.tirith_security.platform.machine", return_value="aarch64"): + assert _detect_target() == "aarch64-unknown-linux-gnu" + + def test_install_returns_termux_native_unsupported_reason(self): + """The install entrypoint must return a distinct failure reason on + Termux so the disk marker / log message can be specific. A generic + unsupported_platform tag would also be retried whenever a binary + appears, but Termux native NEVER works — keep the marker sticky.""" + from tools.tirith_security import _install_tirith + with patch.dict(os.environ, {"PREFIX": "/data/data/com.termux/files/usr"}), \ + patch("tools.tirith_security.platform.system", return_value="Linux"), \ + patch("tools.tirith_security.platform.machine", return_value="aarch64"): + path, reason = _install_tirith() + assert path is None + assert reason == "termux_native_unsupported" + + # --------------------------------------------------------------------------- # HERMES_HOME isolation # --------------------------------------------------------------------------- diff --git a/tools/tirith_security.py b/tools/tirith_security.py index f40da60e52d4..261812addc25 100644 --- a/tools/tirith_security.py +++ b/tools/tirith_security.py @@ -213,6 +213,19 @@ def _hermes_bin_dir() -> str: return d +def _is_termux_native() -> bool: + """Detect a non-proot Termux environment on Android. + + Termux sets PREFIX=/data/data/com.termux/files/usr and ships Bionic + libc with shared objects under that prefix — there is no glibc and + no /lib/ld-linux-*.so.* dynamic linker. Inside a proot Ubuntu (or + similar chroot) running on top of Termux, PREFIX is rewritten to + the chroot's /usr and a real glibc is available, so this check + returns False there. + """ + return os.environ.get("PREFIX", "").startswith("/data/data/com.termux/files/") + + def _detect_target() -> str | None: """Return the Rust target triple for the current platform, or None. @@ -223,7 +236,19 @@ def _detect_target() -> str | None: system = platform.system() machine = platform.machine().lower() - # Android (Termux) is ABI-compatible with Linux — reuse Linux binaries. + # Termux native (Bionic libc) is NOT ABI-compatible with the + # unknown-linux-gnu binaries — the prebuilt tirith ELF embeds + # /lib/ld-linux-aarch64.so.1 (or x86_64 equivalent) as its dynamic + # interpreter, which doesn't exist on Termux's filesystem. Spawning + # the binary then fails with ENOENT and a misleading message that + # points at the tirith path itself even though only the interpreter + # is missing. Skip auto-install and let the user run inside a proot + # Ubuntu chroot or set TIRITH_ENABLED=false. + if _is_termux_native(): + return None + + # Android Python outside Termux native (proot Ubuntu, Pydroid w/ glibc) + # is ABI-compatible with Linux — reuse Linux binaries. if system == "Darwin": plat = "apple-darwin" elif system in {"Linux", "Android"}: @@ -364,6 +389,14 @@ def _install_tirith(*, log_failures: bool = True) -> tuple[str | None, str]: target = _detect_target() if not target: + if _is_termux_native(): + logger.info( + "tirith auto-install: Termux native (Bionic libc) is unsupported; " + "the linux-gnu binary's dynamic interpreter (/lib/ld-linux-*.so.*) " + "is not present on Termux. Run inside a proot Ubuntu chroot, " + "install tirith manually, or set TIRITH_ENABLED=false." + ) + return None, "termux_native_unsupported" logger.info("tirith auto-install: unsupported platform %s/%s", platform.system(), platform.machine()) return None, "unsupported_platform"