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="default-deny|owner allowlist"): 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()