refactor(update): psutil Android installer extracts through the shared safe-tar guard

hermes_cli/psutil_android carried its own tar path-traversal / link-member
guard (a 0.87 copy of archive_safe.safe_extract_targz). One guard for every
tar.gz we extract; the installer keeps raising PsutilAndroidInstallError so
its callers' except clauses are unchanged.

Behavior change: the psutil path now also rejects Windows-absolute and
backslash-smuggled member names (archive_safe.normalize_archive_parts), and
chmod failures on extracted files are suppressed identically.
This commit is contained in:
teknium1
2026-09-12 20:13:56 -07:00
committed by Teknium
parent 3ef8b384a9
commit 30657d197d
2 changed files with 21 additions and 35 deletions

View File

@@ -2,9 +2,9 @@
from __future__ import annotations
import shutil
import tarfile
from pathlib import Path, PurePosixPath
from pathlib import Path
from hermes_cli.archive_safe import safe_extract_targz
# Pinned to a version whose marker line patches cleanly; bump when upstream changes its shape.
PSUTIL_URL = (
@@ -20,40 +20,12 @@ class PsutilAndroidInstallError(RuntimeError):
"""Raised when the pinned psutil sdist is missing or unsafe."""
def _normalize_member_parts(member_name: str) -> tuple[str, ...]:
path = PurePosixPath(member_name)
parts = tuple(part for part in path.parts if part not in ("", "."))
if path.is_absolute() or ".." in parts or not parts:
raise PsutilAndroidInstallError(f"Unsafe archive member path: {member_name!r}")
return parts
def _safe_extract_tar_gz(archive: Path, destination: Path) -> None:
"""Extract a tar.gz without allowing traversal or link members."""
with tarfile.open(archive, "r:gz") as tf:
for member in tf.getmembers():
parts = _normalize_member_parts(member.name)
target = destination.joinpath(*parts)
if member.isdir():
target.mkdir(parents=True, exist_ok=True)
continue
if not member.isfile():
raise PsutilAndroidInstallError(f"Unsupported archive member type: {member.name}")
target.parent.mkdir(parents=True, exist_ok=True)
extracted = tf.extractfile(member)
if extracted is None:
raise PsutilAndroidInstallError(f"Cannot read archive member: {member.name}")
with extracted, open(target, "wb") as dst:
shutil.copyfileobj(extracted, dst)
try:
target.chmod(member.mode & 0o777)
except OSError:
pass
def prepare_patched_psutil_sdist(archive: Path, destination: Path) -> Path:
"""Safely extract the pinned psutil sdist and patch it for Android."""
_safe_extract_tar_gz(archive, destination)
try:
safe_extract_targz(archive, destination) # rejects traversal, links and device nodes
except ValueError as exc:
raise PsutilAndroidInstallError(str(exc)) from exc
src_roots = [path for path in destination.iterdir() if path.is_dir() and path.name.startswith("psutil-")]
if not src_roots:
raise PsutilAndroidInstallError("psutil sdist did not contain a psutil-* directory")

View File

@@ -64,6 +64,20 @@ def test_prepare_patched_psutil_sdist_rejects_symlink_member(tmp_path):
assert not (tmp_path / "outside" / "_common.py").exists()
def test_prepare_patched_psutil_sdist_rejects_traversal_member(tmp_path):
"""A ``..`` member must be refused the same way the shared archive guard refuses it
(one traversal check for every tar.gz we extract), surfaced as the installer's own error."""
archive = tmp_path / "evil.tar.gz"
with tarfile.open(archive, "w:gz") as tf:
_add_dir(tf, "psutil-7.2.2")
_add_file(tf, "psutil-7.2.2/../escaped.py", "x")
with pytest.raises(PsutilAndroidInstallError, match="Unsafe archive member path"):
prepare_patched_psutil_sdist(archive, tmp_path / "extract")
assert not (tmp_path / "escaped.py").exists()
def test_install_psutil_android_compat_uses_patched_tree(tmp_path):
"""Updater path should install from the patched temporary sdist tree."""
archive = tmp_path / "psutil.tar.gz"