Files
EvoScientist-Multi/tests/test_native_sandbox.py
T
m4 5d893c1dc6
Docker / build (push) Has been cancelled
Build / build (push) Has been cancelled
Lint / ruff (push) Has been cancelled
Test / pytest (ubuntu-latest, 3.11) (push) Has been cancelled
Test / pytest (ubuntu-latest, 3.12) (push) Has been cancelled
Test / pytest (windows-latest, 3.11) (push) Has been cancelled
Test / pytest (windows-latest, 3.12) (push) Has been cancelled
[verified] fix: close 0.3.0 release security gaps
2026-09-03 10:42:46 +08:00

263 lines
8.8 KiB
Python

from __future__ import annotations
import json
import os
import sys
import types
from pathlib import Path
import pytest
import EvoScientist.native_sandbox as sandbox
def _installation(tmp_path: Path) -> sandbox.NativeSandboxInstallation:
package = tmp_path / "package"
package.mkdir()
srt = tmp_path / "srt"
srt.touch(mode=0o700)
return sandbox.NativeSandboxInstallation(
srt=srt,
package_root=package,
path_env="/usr/bin:/bin",
system_read_paths=("/usr", "/bin", "/dev/null"),
)
def test_existing_read_paths_resolve_and_deduplicate_symlink_aliases(tmp_path: Path):
usr = tmp_path / "usr"
usr.mkdir()
bin_alias = tmp_path / "bin"
bin_alias.symlink_to(usr, target_is_directory=True)
missing = tmp_path / "missing"
paths = sandbox._existing_resolved_paths((str(usr), str(bin_alias), str(missing)))
assert paths == (str(usr.resolve()),)
def test_packaged_runtime_assets_drive_user_data_install(monkeypatch, tmp_path: Path):
monkeypatch.delenv("EVOSCIENTIST_NATIVE_SANDBOX_ROOT", raising=False)
monkeypatch.setattr("EvoScientist.paths.DATA_DIR", tmp_path)
assets = sandbox._runtime_assets_root()
assert (assets / "package.json").is_file()
assert (assets / "package-lock.json").is_file()
assert (assets / "patch_merged_usr.py").is_file()
assert sandbox._runtime_root() == (tmp_path / "runtime" / "native-sandbox")
def test_runtime_installer_uses_lockfile_and_applies_packaged_patch(
monkeypatch, tmp_path: Path
):
root = tmp_path / "runtime" / "native-sandbox"
calls: list[list[str]] = []
def fake_run(command, *, cwd, **_kwargs):
calls.append(command)
package = (
Path(cwd)
/ "node_modules"
/ "@anthropic-ai"
/ "sandbox-runtime"
)
target = package / "dist" / "sandbox" / "linux-sandbox-utils.js"
target.parent.mkdir(parents=True)
package.joinpath("package.json").write_text(
json.dumps({"version": sandbox._PINNED_SRT_VERSION}), encoding="utf-8"
)
source = sandbox._runtime_assets_root().joinpath("patch_merged_usr.py")
namespace: dict[str, object] = {}
exec(source.read_text(encoding="utf-8"), namespace)
target.write_text(
namespace["_ORIGINAL"] + namespace["_TMPFS_ORIGINAL"],
encoding="utf-8",
)
binary = Path(cwd) / "node_modules" / ".bin" / "srt"
binary.parent.mkdir(parents=True)
binary.write_text("#!/bin/sh\n", encoding="ascii")
binary.chmod(0o700)
return types.SimpleNamespace(returncode=0, stdout="", stderr="")
monkeypatch.setattr(sandbox, "_runtime_root", lambda: root)
monkeypatch.setattr(sandbox.shutil, "which", lambda name, path=None: f"/bin/{name}")
monkeypatch.setattr(sandbox.subprocess, "run", fake_run)
installed = sandbox._ensure_runtime_installed()
assert installed == root
assert calls == [
[
"/bin/npm",
"ci",
"--ignore-scripts",
"--omit=dev",
"--no-audit",
"--no-fund",
]
]
assert json.loads((root / "package-lock.json").read_text(encoding="utf-8"))[
"lockfileVersion"
] == 3
patched = root.joinpath(
"node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/"
"linux-sandbox-utils.js"
).read_text(encoding="utf-8")
assert "rootChildIsAllowedSymlink" in patched
assert "--remount-ro" in patched
assert os.access(root / "node_modules" / ".bin" / "srt", os.X_OK)
def test_runtime_installer_preserves_existing_runtime_when_reinstall_fails(
monkeypatch, tmp_path: Path
):
root = tmp_path / "runtime" / "native-sandbox"
root.mkdir(parents=True)
sentinel = root / "keep.txt"
sentinel.write_text("existing", encoding="utf-8")
monkeypatch.setattr(sandbox, "_runtime_root", lambda: root)
monkeypatch.setattr(sandbox, "_runtime_install_complete", lambda candidate: False)
monkeypatch.setattr(sandbox.shutil, "which", lambda name, path=None: f"/bin/{name}")
def failed_run(*_args, **_kwargs):
return types.SimpleNamespace(returncode=1, stdout="", stderr="install failed")
monkeypatch.setattr(sandbox.subprocess, "run", failed_run)
with pytest.raises(sandbox.NativeSandboxUnavailable, match="npm ci failed"):
sandbox._ensure_runtime_installed()
assert sentinel.read_text(encoding="utf-8") == "existing"
assert not list(root.parent.glob(".native-sandbox-install-*"))
def test_policy_denies_root_and_only_writes_scope_and_command_tmp(tmp_path: Path):
files = tmp_path / "files"
command_tmp = tmp_path / "runtime" / "tmp" / "run"
files.mkdir()
command_tmp.mkdir(parents=True)
policy = sandbox._sandbox_settings(_installation(tmp_path), files, command_tmp)
assert policy["filesystem"]["denyRead"] == ["/"]
assert str(files) in policy["filesystem"]["allowRead"]
assert str(command_tmp) in policy["filesystem"]["allowRead"]
assert policy["filesystem"]["allowWrite"] == [
str(files),
str(command_tmp),
"/dev/null",
]
assert policy["filesystem"]["denyWrite"] == [
str(files / "uploads"),
"/tmp/claude",
"/private/tmp/claude",
"/dev/tty",
"/dev/dtracehelper",
"/dev/autofs_nowait",
]
assert policy["network"]["allowedDomains"] == []
assert policy["network"]["allowAllUnixSockets"] is False
assert policy["allowAppleEvents"] is False
assert "control" not in json.dumps(policy)
def test_weaker_nested_mode_requires_explicit_environment_opt_in(tmp_path: Path, monkeypatch):
monkeypatch.delenv("EVOSCIENTIST_NATIVE_SANDBOX_WEAKER_NESTED", raising=False)
files = tmp_path / "files"
command_tmp = tmp_path / "runtime" / "tmp" / "run"
files.mkdir()
command_tmp.mkdir(parents=True)
installation = _installation(tmp_path)
assert sandbox._sandbox_settings(installation, files, command_tmp)[
"enableWeakerNestedSandbox"
] is False
monkeypatch.setenv("EVOSCIENTIST_NATIVE_SANDBOX_WEAKER_NESTED", "true")
assert sandbox._sandbox_settings(installation, files, command_tmp)[
"enableWeakerNestedSandbox"
] is True
def test_network_preflight_probe_accepts_kernel_denied_unix_socket(monkeypatch):
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
class FakeSocket:
def __init__(self, family=None, *_args):
if family == 1:
raise PermissionError("blocked by seccomp")
def connect_ex(self, _address):
return 1
def close(self):
return None
fake_socket = types.SimpleNamespace(
AF_UNIX=1,
socket=lambda family=None, *args: FakeSocket(family, *args),
)
monkeypatch.setitem(sys.modules, "socket", fake_socket)
namespace: dict[str, object] = {}
exec(sandbox._network_preflight_probe(1234, Path("/blocked.sock")), namespace)
def test_clean_environment_does_not_inherit_secrets(tmp_path: Path, monkeypatch):
command_tmp = tmp_path / "tmp"
(command_tmp / "home").mkdir(parents=True)
(command_tmp / "tmp").mkdir()
monkeypatch.setenv("OPENAI_API_KEY", "secret")
environment = sandbox._clean_environment(_installation(tmp_path), command_tmp)
assert set(environment) == {"PATH", "HOME", "TMPDIR", "WORKSPACE", "LANG", "LC_ALL"}
assert "OPENAI_API_KEY" not in environment
assert environment["WORKSPACE"] == "."
def test_executor_requires_control_directory_outside_files(tmp_path: Path):
files = tmp_path / "files"
files.mkdir()
runtime = files / "runtime"
runtime.mkdir()
with pytest.raises(sandbox.NativeSandboxUnavailable):
sandbox.NativeSandboxExecutor(files, runtime)
def test_readiness_is_cached_and_failure_is_fail_closed(monkeypatch):
sandbox._reset_native_sandbox_readiness_for_tests()
calls = {"install": 0, "preflight": 0}
def install():
calls["install"] += 1
return object()
def preflight(_installation):
calls["preflight"] += 1
monkeypatch.setattr(sandbox, "_assert_install_contract", install)
monkeypatch.setattr(sandbox, "_run_preflight", preflight)
sandbox.ensure_native_sandbox_ready()
sandbox.ensure_native_sandbox_ready()
assert calls == {"install": 1, "preflight": 1}
sandbox._reset_native_sandbox_readiness_for_tests()
monkeypatch.setattr(
sandbox,
"_run_preflight",
lambda _installation: (_ for _ in ()).throw(
sandbox.NativeSandboxUnavailable("failed once")
),
)
with pytest.raises(sandbox.NativeSandboxUnavailable, match="failed once"):
sandbox.ensure_native_sandbox_ready()
with pytest.raises(sandbox.NativeSandboxUnavailable, match="failed once"):
sandbox.ensure_native_sandbox_ready()
assert calls["install"] == 2
sandbox._reset_native_sandbox_readiness_for_tests()