fix(pm): retain chained library aliases on no-symlink hosts

Resolve deferred relative file links until no further alias can be materialized. Keep containment checks and leave unresolved cross-package copyright links alone.
This commit is contained in:
ethernet
2026-09-06 15:49:41 -04:00
parent 203c0a96aa
commit e9dfe6f7d8
2 changed files with 36 additions and 8 deletions

View File

@@ -288,14 +288,23 @@ class DebPackage(Package):
target.chmod(member.mode & 0o777)
except OSError:
pass
for _linkname, target, resolved_path in deferred_links:
if not _contained(resolved_path):
raise InstallError(
self.name,
f"symlink {target.name} resolves outside the staged tree",
)
if not (target.exists() or target.is_symlink()) and resolved_path.is_file():
shutil.copy2(resolved_path, target)
while deferred_links:
pending = []
for linkname, target, resolved_path in deferred_links:
if not _contained(resolved_path):
raise InstallError(
self.name,
f"symlink {target.name} resolves outside the staged tree",
)
if target.exists() or target.is_symlink():
continue
if resolved_path.is_file():
shutil.copy2(resolved_path, target)
else:
pending.append((linkname, target, resolved_path))
if len(pending) == len(deferred_links):
break
deferred_links = pending
# Termux debs carry owner-only modes across the whole tree (700 on
# binaries n libs, 600 on stdlib .py files) -- postinst would
# normalize on a real phone, but pm extracts without postinst, and

View File

@@ -65,6 +65,25 @@ def _symlink_member(name: str, linkname: str) -> tarfile.TarInfo:
return info
def test_chained_aliases_survive_without_symlink_support(tmp_path, monkeypatch):
def unavailable(*args, **kwargs):
raise OSError("symlinks unavailable")
monkeypatch.setattr(Path, "symlink_to", unavailable)
deb = tmp_path / "aliases.deb"
lib = "data/data/com.termux/files/usr/lib"
_build_deb(deb, [
_symlink_member(f"{lib}/libexample.so", "libexample.so.1"),
_symlink_member(f"{lib}/libexample.so.1", "libexample.so.1.2"),
(f"{lib}/libexample.so.1.2", b"library payload"),
])
staged = tmp_path / "staged"
staged.mkdir()
_P().unpack(deb, staged, "linux-arm64-bionic")
for name in ("libexample.so", "libexample.so.1", "libexample.so.1.2"):
assert (staged / lib / name).read_bytes() == b"library payload"
def test_absolute_symlink_target_rejected(tmp_path: Path):
"""A member whose symlink target is ABSOLUTE must be refused outright:
the link points outside the staged tree the moment it is created."""