Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions src/openenv/validation/local.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,24 @@ def _runtime_environment(
return process_env


def _subject_process_kwargs() -> dict[str, Any]:
"""Return POSIX identity controls supplied by a trusted isolated runner."""
raw_uid = os.environ.get("OPENENV_VALIDATION_SUBJECT_UID")
raw_gid = os.environ.get("OPENENV_VALIDATION_SUBJECT_GID")
if raw_uid is None and raw_gid is None:
return {}
if os.name != "posix" or raw_uid is None or raw_gid is None:
raise RuntimeError("The isolated subject identity is incomplete or unsupported")
try:
uid = int(raw_uid)
gid = int(raw_gid)
except ValueError as exc:
raise RuntimeError("The isolated subject identity is malformed") from exc
if uid <= 0 or gid <= 0:
raise RuntimeError("The isolated subject identity must be unprivileged")
return {"user": uid, "group": gid, "extra_groups": ()}


def _ensure_port_available(port: int) -> None:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe:
probe.settimeout(0.25)
Expand Down Expand Up @@ -283,6 +301,13 @@ def _launch_environment(env_path: Path, timeout_s: float) -> Iterator[str]:
base_url = f"http://127.0.0.1:{port}"
_ensure_port_available(port)
with TemporaryDirectory(prefix="openenv-validation-") as temporary_home:
subject_process_kwargs = _subject_process_kwargs()
if subject_process_kwargs:
os.chown(
temporary_home,
subject_process_kwargs["user"],
subject_process_kwargs["group"],
)
process_env = _runtime_environment(
env_path, temporary_home=Path(temporary_home)
)
Expand All @@ -293,6 +318,7 @@ def _launch_environment(env_path: Path, timeout_s: float) -> Iterator[str]:
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True,
**subject_process_kwargs,
)
try:
deadline = time.monotonic() + timeout_s
Expand Down
26 changes: 22 additions & 4 deletions src/openenv/validation/remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
DEFAULT_SANDBOX_FLAVOR = "cpu-basic"
_REMOTE_REVISION_ARCHIVE = "/tmp/openenv-revision.tar.gz"
_REMOTE_VALIDATOR_ARCHIVE = "/tmp/openenv-validator.tar.gz"
_REMOTE_REPORT = "/workspace/validation-report.json"
_REMOTE_REPORT = "/workspace/trusted/validation-report.json"
_REVISION_PATTERN = re.compile(r"(?:[0-9a-f]{40}|[0-9a-f]{64})")
_EXCLUDED_PARTS = frozenset(
{
Expand Down Expand Up @@ -195,13 +195,29 @@ def _remote_command(
runtime_timeout="$5"
environment_root=/workspace/environment
validator_root=/workspace/validator
mkdir -p "$environment_root" "$validator_root" || exit 70
trusted_root=/workspace/trusted
subject_root=/workspace/subject
subject_site="$subject_root/site-packages"
subject_home="$subject_root/home"
subject_uid=10001
subject_gid=10001
mkdir -p "$environment_root" "$validator_root" "$trusted_root" || exit 70
chmod 700 "$trusted_root" || exit 70
tar -xzf "$revision_archive" -C "$environment_root" || exit 70
tar -xzf "$validator_archive" -C "$validator_root" || exit 70
python -m pip install --disable-pip-version-check --no-input "$validator_root" || exit 70
python -m pip install --disable-pip-version-check --no-input "$environment_root" || exit 71
chown -R 0:0 "$environment_root" "$validator_root" "$trusted_root" || exit 70
chmod -R a+rX,go-w "$environment_root" "$validator_root" || exit 70
mkdir -p "$subject_site" "$subject_home" || exit 70
chown -R "$subject_uid:$subject_gid" "$subject_root" || exit 70
setpriv --reuid="$subject_uid" --regid="$subject_gid" --clear-groups \
env HOME="$subject_home" python -m pip install --disable-pip-version-check \
--no-input --target "$subject_site" "$environment_root" || exit 71
set +e
PYTHONPATH="$validator_root/src" python -m openenv.cli.__main__ validate \
OPENENV_VALIDATION_SUBJECT_UID="$subject_uid" \
OPENENV_VALIDATION_SUBJECT_GID="$subject_gid" \
PYTHONPATH="$validator_root/src:$subject_site" \
python -m openenv.cli.__main__ validate \
"$environment_root" --profile "$profile" --json --output "$report_path" \
--timeout "$runtime_timeout"
validation_status=$?
Expand Down Expand Up @@ -575,5 +591,7 @@ def run_remote_validation(
) from exc
finally:
if sandbox is not None:
with suppress(Exception):
sandbox.kill()
with suppress(Exception):
sandbox.close()
17 changes: 17 additions & 0 deletions tests/test_validation/test_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from openenv.validation.local import (
_runtime_environment,
_server_command,
_subject_process_kwargs,
run_local_validation,
)
from openenv.validation.models import (
Expand Down Expand Up @@ -255,3 +256,19 @@ def test_server_command_honors_manifest_port(tmp_path: Path) -> None:
def test_static_profile_rejects_url_in_public_api() -> None:
with pytest.raises(ValueError, match="local source"):
run_local_validation("https://example.com", profile=ValidationProfile.STATIC)


def test_remote_subject_process_drops_to_declared_unprivileged_identity() -> None:
with patch.dict(
os.environ,
{
"OPENENV_VALIDATION_SUBJECT_UID": "10001",
"OPENENV_VALIDATION_SUBJECT_GID": "10002",
},
clear=False,
):
assert _subject_process_kwargs() == {
"user": 10001,
"group": 10002,
"extra_groups": (),
}
5 changes: 5 additions & 0 deletions tests/test_validation/test_remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,8 @@ def write_download(_remote_path: str, local_path: str | Path) -> None:
command_text = " ".join(command) if isinstance(command, list) else command
assert "publish" in command_text
assert "validation" in command_text
assert "setpriv" in command_text
assert "/workspace/trusted/validation-report.json" in command_text
for remote_path in uploads:
assert remote_path in command_text
sandbox.files.download.assert_called_once()
Expand All @@ -201,6 +203,7 @@ def write_download(_remote_path: str, local_path: str | Path) -> None:
assert report.certified is False
assert report.certification_eligible is False
assert report.results[0].criterion_id == "source.validation_spec"
sandbox.kill.assert_called_once_with()
sandbox.close.assert_called_once_with()


Expand Down Expand Up @@ -233,6 +236,7 @@ def test_remote_validation_command_failure_is_clean_and_closes_sandbox(
assert "failed" in message
assert "hf_1234567890abcdef" not in message
sandbox.files.download.assert_not_called()
sandbox.kill.assert_called_once_with()
sandbox.close.assert_called_once_with()


Expand Down Expand Up @@ -264,4 +268,5 @@ def write_download(_remote_path: str, local_path: str | Path) -> None:
assert "report" in message
assert "invalid" in message or "malformed" in message
assert "not-json" not in message
sandbox.kill.assert_called_once_with()
sandbox.close.assert_called_once_with()