Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions tests/tools/test_tirith_security.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
# ---------------------------------------------------------------------------
Expand Down
35 changes: 34 additions & 1 deletion tools/tirith_security.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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"}:
Expand Down Expand Up @@ -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"
Expand Down
Loading