mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-11-01 00:27:20 -05:00
fix: bind only proven Python trees in the sandbox
The interpreter bind took each link's grandparent, so ~/.local/bin/python3 exposed all of ~/.local inside the sandbox. A tree is now bound only when it is a Python prefix (lib/pythonX.Y) or a venv (pyvenv.cfg), never / or the home folder or above it. Every other link is bound as a single file, and a path inside a bound tree is not bound again, since bwrap cannot mount over a link in a read-only tree.
This commit is contained in:
1 parent
e90fb85050
commit
ed61e42f3f
2 files changed
+71
-12
No files matched your search
+38
-12
@@ -682,27 +682,53 @@ def _bwrap_base(bwrap: str, network: bool) -> list[str]:
|
||||
return argv
|
||||
|
||||
|
||||
def _python_prefix(path: Path) -> bool:
|
||||
"""A Python installation or a venv, and nothing wider: it holds
|
||||
lib/pythonX.Y or a pyvenv.cfg, and it is neither / nor the home folder
|
||||
nor a folder above it."""
|
||||
home = Path.home().resolve()
|
||||
resolved = path.resolve()
|
||||
if resolved == Path("/") or resolved == home or resolved in home.parents:
|
||||
return False
|
||||
version = f"python{sys.version_info.major}.{sys.version_info.minor}"
|
||||
return (path / "pyvenv.cfg").is_file() or (path / "lib" / version).is_dir()
|
||||
|
||||
|
||||
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."""
|
||||
"""What the worker's Python needs inside the sandbox when the system
|
||||
binds do not hold it: CI's setup-python under /opt/hostedtoolcache, a
|
||||
venv, pyenv or uv. Without it the worker could not start, and every run
|
||||
crashed.
|
||||
|
||||
Only Python installation prefixes are bound whole (sys.prefix,
|
||||
sys.base_prefix and the prefix of each link's target, each proven by
|
||||
lib/pythonX.Y or pyvenv.cfg). Every link on the way to the interpreter
|
||||
is bound as itself, a single file, so ~/.local/bin/python3 never exposes
|
||||
~/.local."""
|
||||
system = ("/usr/", "/bin/", "/lib")
|
||||
found: list[Path] = []
|
||||
candidates = [Path(sys.prefix), Path(sys.base_prefix)]
|
||||
|
||||
def add(path: Path) -> None:
|
||||
path = Path(os.path.abspath(path))
|
||||
# Inside a tree already bound, a path is there already: bwrap cannot
|
||||
# mount over a link in a read-only tree, and the run would not start.
|
||||
if any(path == bound or bound in path.parents for bound in found):
|
||||
return
|
||||
if not str(path).startswith(system) and path.exists():
|
||||
found.append(path)
|
||||
|
||||
for prefix in (Path(sys.prefix), Path(sys.base_prefix)):
|
||||
if _python_prefix(prefix):
|
||||
add(prefix)
|
||||
hop = Path(sys.executable)
|
||||
for _ in range(16):
|
||||
candidates.append(hop.parent.parent)
|
||||
if _python_prefix(hop.parent.parent):
|
||||
add(hop.parent.parent)
|
||||
if not hop.is_symlink():
|
||||
break
|
||||
add(hop)
|
||||
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
|
||||
|
||||
|
||||
|
||||
@@ -432,10 +432,13 @@ class TheInterpreterIsInTheSandbox(unittest.TestCase):
|
||||
real = root / "uv" / "cpython-3.13.12" / "bin"
|
||||
real.mkdir(parents=True)
|
||||
(real / "python3.13").write_text("")
|
||||
version = f"python{sys.version_info.major}.{sys.version_info.minor}"
|
||||
(root / "uv" / "cpython-3.13.12" / "lib" / version).mkdir(parents=True)
|
||||
alias = root / "uv" / "cpython-3.13"
|
||||
alias.symlink_to(root / "uv" / "cpython-3.13.12")
|
||||
venv = root / "venv" / "bin"
|
||||
venv.mkdir(parents=True)
|
||||
(root / "venv" / "pyvenv.cfg").write_text("home = x\n")
|
||||
(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")), \
|
||||
@@ -444,6 +447,36 @@ class TheInterpreterIsInTheSandbox(unittest.TestCase):
|
||||
self.assertIn(root / "venv", paths)
|
||||
self.assertIn(alias, paths)
|
||||
self.assertIn(root / "uv" / "cpython-3.13.12", paths)
|
||||
# The venv's own link sits in the venv, bound already.
|
||||
self.assertNotIn(venv / "python", paths)
|
||||
|
||||
def test_a_link_in_a_shared_folder_binds_only_itself(self):
|
||||
"""~/.local/bin/python3 must not expose ~/.local: only the link file
|
||||
and the proven Python prefix behind it are bound."""
|
||||
from unittest import mock # noqa: PLC0415
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
prefix = root / "share" / "cpython"
|
||||
(prefix / "bin").mkdir(parents=True)
|
||||
(prefix / "bin" / "python3").write_text("")
|
||||
version = f"python{sys.version_info.major}.{sys.version_info.minor}"
|
||||
(prefix / "lib" / version).mkdir(parents=True)
|
||||
local = root / "dotlocal"
|
||||
(local / "bin").mkdir(parents=True)
|
||||
(local / "secrets.txt").write_text("x")
|
||||
(local / "bin" / "python3").symlink_to(prefix / "bin" / "python3")
|
||||
with mock.patch.object(core_probe.sys, "executable", str(local / "bin" / "python3")), \
|
||||
mock.patch.object(core_probe.sys, "prefix", str(prefix)), \
|
||||
mock.patch.object(core_probe.sys, "base_prefix", str(prefix)):
|
||||
paths = core_probe.interpreter_paths()
|
||||
self.assertNotIn(local, paths)
|
||||
self.assertIn(local / "bin" / "python3", paths)
|
||||
self.assertIn(prefix, paths)
|
||||
|
||||
def test_home_and_root_are_never_a_prefix(self):
|
||||
self.assertFalse(core_probe._python_prefix(Path("/")))
|
||||
self.assertFalse(core_probe._python_prefix(Path.home()))
|
||||
|
||||
def test_a_link_is_mounted_at_its_own_path(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
|
||||
Reference in new issue
Block a user