mirror of
https://github.com/elder-plinius/OBLITERATUS.git
synced 2026-08-17 16:37:30 +02:00
181 lines
6.6 KiB
Python
181 lines
6.6 KiB
Python
"""Mutation-safe contracts for remote input and shell construction."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import shlex
|
|
|
|
import pytest
|
|
|
|
from obliteratus.remote_contracts import (
|
|
normalize_gpu_selection,
|
|
parse_remote_target,
|
|
remote_python_command,
|
|
remote_scp_spec,
|
|
validate_remote_settings,
|
|
)
|
|
|
|
|
|
VALID_REMOTE_SETTINGS = {
|
|
"host": "compute.example",
|
|
"user": "runner",
|
|
"port": 22,
|
|
"remote_dir": "/tmp/obliteratus",
|
|
"python": "python3",
|
|
"gpus": None,
|
|
"install_source": "obliteratus",
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("raw", "expected"),
|
|
[(None, None), ("all", "all"), ("ALL", "all"), ("0", "0"), (" 02, 0,11 ", "2,0,11")],
|
|
)
|
|
def test_gpu_selection_normalizes_public_values(raw, expected):
|
|
assert normalize_gpu_selection(raw) == expected
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("raw", "message"),
|
|
[
|
|
("", "remote gpus must be a non-empty string"),
|
|
("\x7f", "remote gpus may not contain control characters"),
|
|
("0,,1", "remote gpus must be 'all' or comma-separated non-negative integers"),
|
|
("-1", "remote gpus must be 'all' or comma-separated non-negative integers"),
|
|
(
|
|
"0; touch /tmp/pwned",
|
|
"remote gpus must be 'all' or comma-separated non-negative integers",
|
|
),
|
|
("gpu0", "remote gpus must be 'all' or comma-separated non-negative integers"),
|
|
("0\n1", "remote gpus may not contain control characters"),
|
|
],
|
|
)
|
|
def test_gpu_selection_rejects_non_device_input(raw, message):
|
|
with pytest.raises(ValueError) as error:
|
|
normalize_gpu_selection(raw)
|
|
assert str(error.value) == message
|
|
|
|
|
|
def test_remote_target_defaults_user_and_parses_single_at_sign():
|
|
assert parse_remote_target("compute.example") == ("root", "compute.example")
|
|
assert parse_remote_target("runner@compute.example") == ("runner", "compute.example")
|
|
assert parse_remote_target(" runner@compute.example ") == ("runner", "compute.example")
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("target", "message"),
|
|
[
|
|
("", "remote target must be a non-empty string"),
|
|
("\x7f", "remote target may not contain control characters"),
|
|
("runner@", "remote host must be a non-empty string"),
|
|
("@host", "remote user must be a non-empty string"),
|
|
("bad user@host", "remote user contains unsupported characters"),
|
|
(
|
|
"runner@-oProxyCommand=evil",
|
|
"remote host must be a host name or address without user or options",
|
|
),
|
|
("runner@host name", "remote host must be a host name or address without user or options"),
|
|
("a@b@host", "remote target may contain at most one user separator"),
|
|
],
|
|
)
|
|
def test_remote_target_rejects_empty_or_option_like_components(target, message):
|
|
with pytest.raises(ValueError) as error:
|
|
parse_remote_target(target)
|
|
assert str(error.value) == message
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("override", "message"),
|
|
[
|
|
({"host": ""}, "remote host must be a non-empty string"),
|
|
(
|
|
{"host": "bad@host"},
|
|
"remote host must be a host name or address without user or options",
|
|
),
|
|
(
|
|
{"host": "bad host"},
|
|
"remote host must be a host name or address without user or options",
|
|
),
|
|
({"user": ""}, "remote user must be a non-empty string"),
|
|
({"user": "-oProxy"}, "remote user contains unsupported characters"),
|
|
({"user": "bad user"}, "remote user contains unsupported characters"),
|
|
({"port": 0}, "remote port must be an integer from 1 through 65535"),
|
|
({"port": 65536}, "remote port must be an integer from 1 through 65535"),
|
|
({"port": True}, "remote port must be an integer from 1 through 65535"),
|
|
({"port": "22"}, "remote port must be an integer from 1 through 65535"),
|
|
({"remote_dir": ""}, "remote directory must be a non-empty string"),
|
|
({"remote_dir": "relative"}, "remote directory must be an absolute POSIX path"),
|
|
({"python": ""}, "remote Python must be a non-empty string"),
|
|
({"install_source": ""}, "remote install source must be a non-empty string"),
|
|
(
|
|
{"install_source": "bad\nsource"},
|
|
"remote install source may not contain control characters",
|
|
),
|
|
],
|
|
)
|
|
def test_remote_settings_reject_invalid_boundaries(override, message):
|
|
values = dict(VALID_REMOTE_SETTINGS)
|
|
values.update(override)
|
|
with pytest.raises(ValueError) as error:
|
|
validate_remote_settings(**values)
|
|
assert str(error.value) == message
|
|
|
|
|
|
@pytest.mark.parametrize("port", [1, 65535])
|
|
def test_remote_settings_accept_port_boundaries(port):
|
|
values = dict(VALID_REMOTE_SETTINGS, port=port, gpus=" 02,0 ")
|
|
assert validate_remote_settings(**values) == "2,0"
|
|
|
|
|
|
def test_remote_python_command_preserves_each_untrusted_value_as_one_argument():
|
|
command = remote_python_command(
|
|
"/opt/python builds/current/python",
|
|
["-m", "obliteratus", "run", "/tmp/a config.yml", "--preset", "x; echo injected"],
|
|
gpus="02,0",
|
|
)
|
|
assert shlex.split(command) == [
|
|
"env",
|
|
"CUDA_VISIBLE_DEVICES=2,0",
|
|
"/opt/python builds/current/python",
|
|
"-m",
|
|
"obliteratus",
|
|
"run",
|
|
"/tmp/a config.yml",
|
|
"--preset",
|
|
"x; echo injected",
|
|
]
|
|
|
|
|
|
def test_remote_python_command_omits_environment_for_all_devices():
|
|
assert shlex.split(remote_python_command("python3", ["-V"], gpus="all")) == ["python3", "-V"]
|
|
assert shlex.split(remote_python_command("python3", ["-V"])) == ["python3", "-V"]
|
|
|
|
|
|
def test_remote_python_command_reports_invalid_python_contract():
|
|
with pytest.raises(ValueError) as error:
|
|
remote_python_command("", ["-V"])
|
|
assert str(error.value) == "remote Python must be a non-empty string"
|
|
|
|
|
|
def test_remote_scp_spec_quotes_remote_paths_and_directory_suffix():
|
|
assert remote_scp_spec("runner@host", "/tmp/result file", directory=True) == (
|
|
"runner@host:'/tmp/result file/'"
|
|
)
|
|
assert remote_scp_spec("runner@host", "/tmp/results/", directory=True) == (
|
|
"runner@host:/tmp/results/"
|
|
)
|
|
assert remote_scp_spec("runner@host", "/tmp/config.yml") == "runner@host:/tmp/config.yml"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("target", "path", "message"),
|
|
[
|
|
("", "/tmp/config.yml", "SSH target must be a non-empty string"),
|
|
("runner@host", "", "remote SCP path must be a non-empty string"),
|
|
("runner@host", "/tmp/bad\x7fpath", "remote SCP path may not contain control characters"),
|
|
],
|
|
)
|
|
def test_remote_scp_spec_reports_invalid_input_contract(target, path, message):
|
|
with pytest.raises(ValueError) as error:
|
|
remote_scp_spec(target, path)
|
|
assert str(error.value) == message
|