mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 03:49:26 +00:00
search-with-proof: root-no-userns isolation mode for gVisor
The kube-ax probe showed bwrap cannot map uids in gVisor as an unprivileged user. As root (gVisor is the outer boundary) the sandbox uses pid/ipc/uts/net/mount namespaces without a user namespace and drops the artefact to its own uid via setpriv; the mode is recorded. The probe reports each namespace kind separately. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
e6871fef9a
commit
03857c9bec
@@ -10,6 +10,9 @@ def test_image_matches_runner_and_publishes_digest():
|
||||
assert runner in engine and runner in image
|
||||
for package in ("python3", "nodejs", "node-typescript", "php-cli", "sqlite3", "bubblewrap", "util-linux", "chromium"):
|
||||
assert package in image
|
||||
assert image.rsplit("USER ", 1)[1].startswith("root\n")
|
||||
assert "--uid 10001 --gid 10001 search" in image
|
||||
assert "setpriv --version" in image
|
||||
assert "17-jdk" in image and "--no-install-recommends" in image
|
||||
assert "rm -rf /var/lib/apt/lists/" in image
|
||||
workflow_text = Path(".github/workflows/engine-image.yml").read_text()
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
"""Root command construction is tested without requiring root privileges."""
|
||||
|
||||
import subprocess
|
||||
from dataclasses import asdict
|
||||
|
||||
import pytest
|
||||
|
||||
from openultrasast.sandbox import SandboxJob
|
||||
from openultrasast.search import _sandbox, probe
|
||||
from openultrasast.search.oracles import BrowserExecutor, Canary, SSRFOracle
|
||||
from openultrasast.search.verify import SideRecord, VerificationRecord
|
||||
|
||||
|
||||
@pytest.mark.parametrize("uid,mode", [(0, "root-no-userns"), (1000, "userns")])
|
||||
def test_selection_and_records(monkeypatch, uid, mode):
|
||||
monkeypatch.setattr(_sandbox.os, "geteuid", lambda: uid)
|
||||
assert _sandbox.isolation_mode() == mode
|
||||
assert asdict(SideRecord("observed", (), 0, ""))["isolation_mode"] == mode
|
||||
assert asdict(VerificationRecord("inconclusive", (), 0, ""))["isolation_mode"] == mode
|
||||
|
||||
|
||||
@pytest.mark.parametrize("pid", [None, 123])
|
||||
def test_root_argv(monkeypatch, tmp_path, pid):
|
||||
monkeypatch.setattr(_sandbox.os, "geteuid", lambda: 0)
|
||||
monkeypatch.setattr(_sandbox.os, "chown", lambda *a, **kw: None)
|
||||
monkeypatch.setattr(_sandbox.os, "killpg", lambda *a: None)
|
||||
commands = []
|
||||
|
||||
class Process:
|
||||
returncode = 0
|
||||
pid = 987
|
||||
|
||||
def __init__(self, argv, **kwargs):
|
||||
commands.append(argv)
|
||||
assert kwargs["pass_fds"]
|
||||
assert kwargs["preexec_fn"]
|
||||
|
||||
def wait(self, **kwargs):
|
||||
return 0
|
||||
|
||||
monkeypatch.setattr(_sandbox.subprocess, "Popen", Process)
|
||||
job = SandboxJob("", ("/bin/true",), tmp_path, {}, 5, 256, 128)
|
||||
_sandbox.run(job, scratch=tmp_path, namespace_pid=pid)
|
||||
argv = commands[0]
|
||||
assert "--unshare-user" not in argv
|
||||
assert not any(a.startswith("--user=") for a in argv)
|
||||
assert {"--unshare-pid", "--unshare-ipc", "--unshare-uts", "--seccomp"} <= set(argv)
|
||||
drop = _sandbox.drop_privileges()
|
||||
start = argv.index(drop[0])
|
||||
assert argv[start : start + len(drop)] == drop
|
||||
assert argv.index("/usr/bin/prlimit") > start
|
||||
assert "--nproc=128:128" in argv
|
||||
if pid:
|
||||
assert "--net=/proc/123/ns/net" in argv
|
||||
else:
|
||||
assert "/usr/bin/nsenter" not in argv
|
||||
|
||||
|
||||
def test_root_browser(monkeypatch):
|
||||
monkeypatch.setattr(_sandbox.os, "geteuid", lambda: 0)
|
||||
monkeypatch.setattr(_sandbox.os, "chown", lambda *a, **kw: None)
|
||||
|
||||
def run(argv, **kwargs):
|
||||
assert "--unshare-user" not in argv and "--unshare-all" not in argv
|
||||
assert "--unshare-net" in argv and "--unshare-pid" in argv
|
||||
assert "--no-sandbox" in argv
|
||||
assert argv.index("/usr/bin/setpriv") < argv.index("/fake/chromium")
|
||||
return subprocess.CompletedProcess(argv, 0, "<html></html>", "")
|
||||
|
||||
monkeypatch.setattr(subprocess, "run", run)
|
||||
browser = BrowserExecutor("/fake/chromium")
|
||||
assert browser.no_sandbox
|
||||
assert not browser("", "nonce")
|
||||
|
||||
|
||||
def test_namespace_diagnostics_precede_failed_preflight(monkeypatch, tmp_path):
|
||||
calls = []
|
||||
|
||||
def run(argv, **kwargs):
|
||||
calls.append(argv)
|
||||
return subprocess.CompletedProcess(argv, 1, "", "denied")
|
||||
|
||||
def fail():
|
||||
assert len(calls) == 8
|
||||
raise _sandbox.IsolationUnavailable("denied")
|
||||
|
||||
monkeypatch.setattr(subprocess, "run", run)
|
||||
monkeypatch.setattr(probe, "image_facts", lambda: {})
|
||||
monkeypatch.setattr(_sandbox, "isolation_check", fail)
|
||||
record = probe.probe(tmp_path)
|
||||
assert record["status"] == "instrument_failure"
|
||||
assert set(record["namespaces"]) == {"user", "pid", "net", "ipc", "uts", "cgroup", "mount", "setpriv"}
|
||||
assert all(row["exit_code"] == 1 and row["stderr"] == "denied" for row in record["namespaces"].values())
|
||||
assert record["isolation_mode"] == _sandbox.isolation_mode()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("uid", [0, 1000])
|
||||
def test_listener_namespace_setup(monkeypatch, tmp_path, uid):
|
||||
import os
|
||||
|
||||
monkeypatch.setattr(_sandbox.os, "geteuid", lambda: uid)
|
||||
read_fd, write_fd = os.pipe()
|
||||
os.write(write_fd, b"[123, 4567]\n")
|
||||
os.close(write_fd)
|
||||
stream = os.fdopen(read_fd)
|
||||
commands = []
|
||||
|
||||
class Process:
|
||||
stdout = stream
|
||||
|
||||
def __init__(self, argv, **kwargs):
|
||||
commands.append(argv)
|
||||
|
||||
def terminate(self):
|
||||
pass
|
||||
|
||||
def wait(self, **kwargs):
|
||||
return 0
|
||||
|
||||
monkeypatch.setattr(subprocess, "Popen", Process)
|
||||
oracle = SSRFOracle()
|
||||
try:
|
||||
env = oracle.prepare(Canary.fresh(tmp_path / "fixture"))
|
||||
assert oracle.namespace_pid == 123
|
||||
assert ":4567/" in env["CALLBACK_URL"]
|
||||
assert "--net" in commands[0]
|
||||
assert ("--user" in commands[0]) == (uid != 0)
|
||||
assert ("--map-root-user" in commands[0]) == (uid != 0)
|
||||
finally:
|
||||
oracle.close()
|
||||
|
||||
|
||||
def test_root_fixture_permissions_and_browser_records(monkeypatch, tmp_path):
|
||||
from openultrasast.sandbox import SandboxResult
|
||||
from openultrasast.search import verify as verifier
|
||||
|
||||
monkeypatch.setattr(_sandbox.os, "geteuid", lambda: 0)
|
||||
monkeypatch.setattr(_sandbox, "isolation_check", lambda **kw: None)
|
||||
sides, demos = probe.materialise(tmp_path / "pair", "path")
|
||||
app_calls = []
|
||||
|
||||
def run(job, *, scratch, mounts, **kwargs):
|
||||
if "/fixture" in mounts:
|
||||
fixture = mounts["/fixture"]
|
||||
assert fixture.name == "app-fixture"
|
||||
original = fixture.parent / "fixture"
|
||||
assert original.stat().st_mode & 0o777 == 0o700
|
||||
assert (original / "canary").stat().st_mode & 0o777 == 0o600
|
||||
assert (fixture / "canary").read_bytes() == (original / "canary").read_bytes()
|
||||
assert fixture.stat().st_mode & 0o777 == 0o555
|
||||
app_calls.append(job)
|
||||
return SandboxResult(0, "", "", False)
|
||||
|
||||
monkeypatch.setattr(_sandbox, "run", run)
|
||||
browser = BrowserExecutor("/fake/chromium")
|
||||
record = verifier.verify(*sides, demos["real"], "path", browser=browser)
|
||||
assert app_calls
|
||||
assert record.isolation_mode == "root-no-userns"
|
||||
assert record.chromium_no_sandbox
|
||||
assert all(s.chromium_no_sandbox and s.isolation_mode == "root-no-userns" for s in record.sides)
|
||||
@@ -4,6 +4,7 @@ from dataclasses import asdict
|
||||
import pytest
|
||||
|
||||
from openultrasast.plane.memory import FileStore
|
||||
from openultrasast.search import _sandbox
|
||||
from openultrasast.search.board import Board
|
||||
from openultrasast.search.budget import SearchBudget
|
||||
from openultrasast.search.coordinator import Coordinator, SearchTask
|
||||
@@ -29,6 +30,7 @@ def test_reason_no_tools_and_bounded(tmp_path):
|
||||
result = Worker(model, repo=tmp_path, demo=tmp_path / "demo")(task())
|
||||
assert result["intents"][0]["description"] == "inspect"
|
||||
assert model.calls[0]["tools"] == []
|
||||
assert result["isolation_mode"] == _sandbox.isolation_mode()
|
||||
assert result["cost_usd"] == 0
|
||||
model = StubModel([reply({"intents": [{"description": "x"}] * 4})])
|
||||
assert Worker(model, repo=tmp_path, demo=tmp_path / "demo")(task())["status"] == "execution_failure"
|
||||
@@ -59,6 +61,7 @@ def test_run_scrubs_model_key_in_real_sandbox(tmp_path, monkeypatch):
|
||||
"timeout_seconds": 2,
|
||||
},
|
||||
)
|
||||
assert result["isolation_mode"] == _sandbox.isolation_mode()
|
||||
assert result["exit_code"] == 0, result
|
||||
assert result["stdout"].splitlines()[0] == "False"
|
||||
assert "must-not-reach-repo" not in json.dumps(result)
|
||||
|
||||
Reference in New Issue
Block a user