Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a0726e-5232-73f2-aaca-2c05ceb62efb
116 lines
4.8 KiB
Python
116 lines
4.8 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="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()
|