sand-boxer/tests/test_bwrap_runtime.py
tegwick d69827aaa2 feat: pin bwrap rein runtimes and isolate private state
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a0726e-5232-73f2-aaca-2c05ceb62efb
2026-09-05 20:36:11 +02:00

116 lines
4.7 KiB
Python

import socket
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from sandboxer.extensions.bwrap import BwrapExtension
from sandboxer.extensions.bwrap_runner import _run
from sandboxer.extensions.runtime import RUNTIME_MOUNT, runtime_digest, verified_runtime
from sandboxer.models import Profile
@pytest.fixture
def artifact(tmp_path):
root = tmp_path / "runtime"
(root / "bin").mkdir(parents=True)
(root / "pyvenv.cfg").write_text("home = /usr/bin\n")
(root / "bin/python3").write_bytes(b"test-only-python")
(root / "bin/python3").chmod(0o755)
return root
def test_runtime_mount_requires_matching_complete_digest(artifact, tmp_path):
config = {"runtime": {"path": str(artifact), "sha256": runtime_digest(artifact)}}
assert verified_runtime(config) == artifact
argv = BwrapExtension(config)._bwrap_argv(str(tmp_path / "workspace"))
position = argv.index(str(artifact))
assert argv[position - 1:position + 2] == ["--ro-bind", str(artifact), RUNTIME_MOUNT]
assert argv[-1] == "--runtime"
(artifact / "bin/python3").write_bytes(b"modified")
with pytest.raises(ValueError, match="digest"):
BwrapExtension(config)._bwrap_argv(str(tmp_path / "workspace"))
def test_mode_change_invalidates_runtime_pin(artifact):
config = {"runtime": {"path": str(artifact), "sha256": runtime_digest(artifact)}}
(artifact / "bin/python3").chmod(0o777)
with pytest.raises(ValueError, match="digest"):
verified_runtime(config)
def test_unexpected_file_invalidates_runtime_pin(artifact):
config = {"runtime": {"path": str(artifact), "sha256": runtime_digest(artifact)}}
(artifact / "injected.py").write_text("unexpected")
with pytest.raises(ValueError, match="digest"):
verified_runtime(config)
def test_runtime_symlinks_must_stay_inside_artifact(artifact, tmp_path):
(artifact / "python").symlink_to("bin/python3")
runtime_digest(artifact)
outside = tmp_path / "outside"
outside.write_text("host data")
(artifact / "escape").symlink_to(outside)
with pytest.raises(ValueError, match="symlink escapes"):
runtime_digest(artifact)
def test_runtime_rejects_socket(artifact):
with socket.socket(socket.AF_UNIX) as control:
control.bind(str(artifact / "host.sock"))
with pytest.raises(ValueError, match="special file"):
runtime_digest(artifact)
@pytest.mark.parametrize("runtime", [{}, {"path": "/tmp"}, {"path": 3, "sha256": "x"}])
def test_incomplete_runtime_config_refuses(runtime):
with pytest.raises(ValueError):
verified_runtime({"runtime": runtime})
def test_source_and_runtime_overlap_refuses_before_copy(artifact):
ext = BwrapExtension({"runtime": {"path": str(artifact),
"sha256": runtime_digest(artifact)}})
profile = Profile(id="profile.test", version="1", extension="ext.bwrap")
with (
patch("sandboxer.extensions.bwrap.shutil.copytree") as copy,
pytest.raises(ValueError, match="source checkout"),
):
ext.provision(profile, {"repo": str(artifact.parent)}, "localhost")
copy.assert_not_called()
@pytest.mark.parametrize("network", [
{"default": "allow", "egress": []},
{"default": "deny", "egress": ["api.example.invalid:443"]},
])
def test_unsupported_network_refuses_before_provisioning(tmp_path, network):
profile = Profile(id="profile.test", version="1", extension="ext.bwrap", network=network)
base = tmp_path / "sandboxes"
with pytest.raises(ValueError, match="empty egress"):
BwrapExtension({"base_dir": str(base)}).provision(profile, {}, "localhost")
assert not base.exists()
def test_runtime_path_is_not_taken_from_command_payload(tmp_path):
process = MagicMock(returncode=0)
process.communicate.return_value = (b"", b"")
payload = {"command": ["rein-aharness", "--help"], "timeout_seconds": 10,
"max_output_bytes": 100, "runtime_path": "/host/source", "env": {"HOME": "/host"}}
with patch("sandboxer.extensions.bwrap_runner.subprocess.Popen", return_value=process) as run:
_run(payload, tmp_path, runtime_enabled=True)
env = run.call_args.kwargs["env"]
assert env["PATH"].split(":")[0] == RUNTIME_MOUNT + "/bin"
assert not Path(env["HOME"]).is_relative_to(tmp_path)
assert "/host" not in str(env)
assert env["PYTHONNOUSERSITE"] == "1"
def test_setup_credentials_refuse_before_provisioning(tmp_path):
profile = Profile(id="profile.test", version="1", extension="ext.bwrap",
setup={"secret_refs": ["test-route"]})
base = tmp_path / "sandboxes"
with pytest.raises(ValueError, match="credential delivery contract"):
BwrapExtension({"base_dir": str(base)}).provision(profile, {}, "localhost")
assert not base.exists()