mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-11-01 00:27:20 -05:00
fix: bind the interpreter into the probe sandbox
The sandbox bound only /usr, /lib and /bin. On the CI runner setup-python puts Python under /opt/hostedtoolcache, so the worker could not start and every core_probe and campaign run crashed (7 tests, plus their replay without jsonschema). A venv, pyenv or uv do the same. The interpreter's prefix and every link on the way to it are now bound read-only, each at its own path. Reproduced with uv's Python 3.13 outside /usr, 92 tests pass under it and under the system Python.
This commit is contained in:
1 parent
06ca83bdca
commit
e90fb85050
2 files changed
+69
-3
No files matched your search
+29
-3
@@ -682,6 +682,30 @@ def _bwrap_base(bwrap: str, network: bool) -> list[str]:
|
||||
return argv
|
||||
|
||||
|
||||
def interpreter_paths() -> list[Path]:
|
||||
"""The trees the worker's Python lives in, when the sandbox's system
|
||||
binds do not hold them: CI's setup-python under /opt/hostedtoolcache, a
|
||||
venv, pyenv or uv. Each link on the way to the interpreter is a path too:
|
||||
a venv's python points at uv's unversioned name, itself a link to the
|
||||
versioned tree. Without them the worker could not start in the sandbox,
|
||||
and every run crashed."""
|
||||
system = ("/usr/", "/bin/", "/lib")
|
||||
found: list[Path] = []
|
||||
candidates = [Path(sys.prefix), Path(sys.base_prefix)]
|
||||
hop = Path(sys.executable)
|
||||
for _ in range(16):
|
||||
candidates.append(hop.parent.parent)
|
||||
if not hop.is_symlink():
|
||||
break
|
||||
target = Path(os.readlink(hop))
|
||||
hop = target if target.is_absolute() else hop.parent / target
|
||||
for path in candidates:
|
||||
path = Path(os.path.abspath(path))
|
||||
if not str(path).startswith(system) and path not in found and path.exists():
|
||||
found.append(path)
|
||||
return found
|
||||
|
||||
|
||||
def sandbox_argv(writable: Sequence[Path], readonly: Sequence[Path] = (),
|
||||
network: bool = False) -> tuple[list[str], str]:
|
||||
"""The bubblewrap prefix a third-party program runs under, and its name.
|
||||
@@ -704,8 +728,10 @@ def sandbox_argv(writable: Sequence[Path], readonly: Sequence[Path] = (),
|
||||
return [], "environment only: bwrap cannot create a sandbox here"
|
||||
argv = _bwrap_base(bwrap, network)
|
||||
for path in readonly:
|
||||
resolved = Path(path).resolve()
|
||||
argv += ["--ro-bind", str(resolved), str(resolved)]
|
||||
# Mounted at the path as the caller names it: a link's own path stays
|
||||
# usable inside, its target's bytes behind it.
|
||||
resolved, named = Path(path).resolve(), Path(os.path.abspath(path))
|
||||
argv += ["--ro-bind", str(resolved), str(named)]
|
||||
for path in writable:
|
||||
resolved = Path(path).resolve()
|
||||
resolved.mkdir(parents=True, exist_ok=True)
|
||||
@@ -756,7 +782,7 @@ def run_once(args: argparse.Namespace, system_dir: Path, scratch: Path, state_ou
|
||||
if strace:
|
||||
command = [strace, "-f", "-qq", "-s", "4096", "-e", "trace=openat,open", "-o", str(trace_path)] + command
|
||||
writable = [scratch] + [p for p in (png_dir, state_out.parent if state_out else None) if p]
|
||||
readonly = [Path(__file__), Path(args.core), system_dir]
|
||||
readonly = [Path(__file__), Path(args.core), system_dir, *interpreter_paths()]
|
||||
if args.content:
|
||||
readonly.append(Path(args.content).resolve().parent)
|
||||
readonly += [Path(value) for _, value in args.env if Path(value).is_absolute() and Path(value).exists()]
|
||||
|
||||
@@ -418,5 +418,45 @@ class Sandbox(unittest.TestCase):
|
||||
self.assertIn("--require-sandbox: environment only: bwrap not found", problems)
|
||||
|
||||
|
||||
|
||||
class TheInterpreterIsInTheSandbox(unittest.TestCase):
|
||||
"""CI's setup-python, a venv and uv put Python outside /usr. The sandbox
|
||||
bound only the system trees, so the worker could not start and every run
|
||||
crashed on the runner."""
|
||||
|
||||
def test_each_link_to_the_interpreter_is_bound(self):
|
||||
from unittest import mock # noqa: PLC0415
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
real = root / "uv" / "cpython-3.13.12" / "bin"
|
||||
real.mkdir(parents=True)
|
||||
(real / "python3.13").write_text("")
|
||||
alias = root / "uv" / "cpython-3.13"
|
||||
alias.symlink_to(root / "uv" / "cpython-3.13.12")
|
||||
venv = root / "venv" / "bin"
|
||||
venv.mkdir(parents=True)
|
||||
(venv / "python").symlink_to(alias / "bin" / "python3.13")
|
||||
with mock.patch.object(core_probe.sys, "executable", str(venv / "python")), \
|
||||
mock.patch.object(core_probe.sys, "prefix", str(root / "venv")), \
|
||||
mock.patch.object(core_probe.sys, "base_prefix", str(root / "uv" / "cpython-3.13.12")):
|
||||
paths = core_probe.interpreter_paths()
|
||||
self.assertIn(root / "venv", paths)
|
||||
self.assertIn(alias, paths)
|
||||
self.assertIn(root / "uv" / "cpython-3.13.12", paths)
|
||||
|
||||
def test_a_link_is_mounted_at_its_own_path(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
target = Path(tmp) / "real"
|
||||
target.mkdir()
|
||||
link = Path(tmp) / "link"
|
||||
link.symlink_to(target)
|
||||
prefix, isolation = core_probe.sandbox_argv([], [link])
|
||||
if isolation != core_probe.SANDBOX:
|
||||
self.skipTest(f"no sandbox here ({isolation})")
|
||||
self.assertIn(["--ro-bind", str(target.resolve()), str(link)],
|
||||
[prefix[i:i + 3] for i in range(len(prefix) - 2)])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user