Add fail-closed Linux process and descriptor primitives
This commit is contained in:
parent
b34421efa1
commit
1468f6b841
1 changed files with 1725 additions and 15 deletions
|
|
@ -26,6 +26,8 @@ NEXTEST_COMMAND = (
|
|||
"--workspace",
|
||||
"--all-features",
|
||||
"--locked",
|
||||
"--filterset",
|
||||
"not binary(capability-drop-helper)",
|
||||
)
|
||||
DOCTEST_COMMAND = (
|
||||
"cargo",
|
||||
|
|
@ -36,6 +38,16 @@ DOCTEST_COMMAND = (
|
|||
"--locked",
|
||||
"--doc",
|
||||
)
|
||||
CAPABILITY_TEST_COMMAND = (
|
||||
"cargo",
|
||||
"test",
|
||||
"--offline",
|
||||
"-p",
|
||||
"lazaret-linux",
|
||||
"--test",
|
||||
"capability-drop-helper",
|
||||
"--locked",
|
||||
)
|
||||
REQUIRED_TOOL_VERSIONS = (
|
||||
("rustc", ("--version",), (0,), re.compile(r"^rustc 1\.90\.0 ")),
|
||||
("cargo", ("--version",), (0,), re.compile(r"^cargo 1\.90\.0 ")),
|
||||
|
|
@ -90,7 +102,27 @@ REQUIRED_DISALLOWED_METHODS = {
|
|||
"std::process::Command::spawn",
|
||||
"std::process::Command::status",
|
||||
}
|
||||
PROCESS_LINT_EXEMPTIONS = frozenset()
|
||||
PROCESS_LINT_ALLOWANCES = {
|
||||
"crates/lazaret-linux/src/process.rs": {
|
||||
"append_to",
|
||||
"send_signal",
|
||||
"acquire",
|
||||
"reap",
|
||||
"acquire_spawned_child",
|
||||
"verify_direct_child_pidfd",
|
||||
"fail_before_pidfd",
|
||||
"fail_after_pidfd_closed",
|
||||
"recover_process",
|
||||
"current_process_identity",
|
||||
"append_parent_identity",
|
||||
"exec_target",
|
||||
},
|
||||
"crates/lazaret-linux/tests/acquisition.rs": {
|
||||
"inherited_barrier_refuses_eof_partial_and_malformed_release",
|
||||
"foreign_non_child_pidfd_is_refused_without_action",
|
||||
},
|
||||
"crates/lazaret-linux/tests/capability-drop-helper.rs": {"main"},
|
||||
}
|
||||
FORBIDDEN_PROCESS_SYSCALLS = tuple(
|
||||
re.compile(pattern)
|
||||
for pattern in (
|
||||
|
|
@ -203,16 +235,37 @@ def audit_workspace_lints(metadata: dict) -> None:
|
|||
f"{package['name']}: package must inherit workspace lints without override",
|
||||
)
|
||||
|
||||
observed_allowances = {}
|
||||
for path in workspace_rust_sources(metadata):
|
||||
relative = str(path.relative_to(ROOT))
|
||||
require(
|
||||
LINT_BYPASS.search(path.read_text(errors="replace")) is None
|
||||
or relative in PROCESS_LINT_EXEMPTIONS,
|
||||
f"process-safety lint exemption is forbidden: {relative}",
|
||||
)
|
||||
source = path.read_text(errors="replace")
|
||||
functions = process_lint_allowances(relative, source)
|
||||
if functions:
|
||||
observed_allowances[relative] = functions
|
||||
require(
|
||||
observed_allowances == PROCESS_LINT_ALLOWANCES,
|
||||
"process-safety lint allowances must match exact reviewed functions",
|
||||
)
|
||||
print("PASS workspace unsafe and process-control lint boundary")
|
||||
|
||||
|
||||
def process_lint_allowances(relative: str, source: str) -> set[str]:
|
||||
functions = set()
|
||||
for exemption in LINT_BYPASS.finditer(source):
|
||||
require(
|
||||
re.match(r"#\s*!", exemption.group()) is None,
|
||||
f"whole-file process lint exemption: {relative}",
|
||||
)
|
||||
following = source[exemption.end() : exemption.end() + 400]
|
||||
function = re.match(
|
||||
r"\s*\)\s*\]\s*(?:#\[[^]]+\]\s*)*(?:pub\s+)?fn\s+([A-Za-z0-9_]+)\s*[<(]",
|
||||
following,
|
||||
)
|
||||
require(function is not None, f"non-function process lint exemption: {relative}")
|
||||
functions.add(function.group(1))
|
||||
return functions
|
||||
|
||||
|
||||
def workspace_rust_sources(metadata: dict) -> list[Path]:
|
||||
packages = {package["id"]: package for package in metadata["packages"]}
|
||||
target_directory = Path(metadata["target_directory"]).resolve()
|
||||
|
|
@ -247,13 +300,8 @@ def audit_dependencies(metadata: dict) -> None:
|
|||
f"unapproved dependency source: {package['name']}",
|
||||
)
|
||||
license_expression = package.get("license") or ""
|
||||
licenses = set(re.findall(r"[A-Za-z0-9.-]+", license_expression)) - {
|
||||
"AND",
|
||||
"OR",
|
||||
"WITH",
|
||||
}
|
||||
require(
|
||||
bool(licenses) and licenses <= allowed_licenses,
|
||||
license_permitted(license_expression, allowed_licenses),
|
||||
f"unapproved dependency license: {package['name']} {license_expression}",
|
||||
)
|
||||
for dependency in package["dependencies"]:
|
||||
|
|
@ -280,6 +328,16 @@ def audit_dependencies(metadata: dict) -> None:
|
|||
print("PASS dependency source, wildcard, and license audit")
|
||||
|
||||
|
||||
def license_permitted(expression: str, allowed: set[str]) -> bool:
|
||||
return bool(expression) and any(
|
||||
all(
|
||||
term.strip(" ()") in allowed
|
||||
for term in re.split(r"\s+AND\s+", alternative)
|
||||
)
|
||||
for alternative in re.split(r"\s+OR\s+", expression)
|
||||
)
|
||||
|
||||
|
||||
def run_models() -> None:
|
||||
tlc = shutil.which("tlc")
|
||||
require(tlc is not None, "TLC must be installed on PATH")
|
||||
|
|
@ -362,7 +420,11 @@ def build_workspace_binaries(metadata: dict) -> dict[str, Path]:
|
|||
|
||||
def validate_process_trace(binary: str, lines: list[str]) -> None:
|
||||
execve_count = sum(line.startswith("execve(") for line in lines)
|
||||
require(execve_count == 1, f"{binary}: unexpected execve surface count {execve_count}")
|
||||
expected_execve = 2 if binary == "lazaret-launch-wrapper" else 1
|
||||
require(
|
||||
execve_count == expected_execve,
|
||||
f"{binary}: unexpected execve surface count {execve_count}",
|
||||
)
|
||||
for line in lines:
|
||||
matches = [pattern.pattern for pattern in FORBIDDEN_PROCESS_SYSCALLS if pattern.search(line)]
|
||||
require(not matches, f"{binary}: forbidden process syscall surface {line}: {matches}")
|
||||
|
|
@ -377,8 +439,7 @@ def audit_process_syscalls(metadata: dict) -> None:
|
|||
with tempfile.TemporaryDirectory(prefix="lazaret-process-syscalls-") as directory:
|
||||
for binary, executable in sorted(binaries.items()):
|
||||
trace_base = Path(directory) / binary
|
||||
result = subprocess.run(
|
||||
[
|
||||
command = [
|
||||
strace,
|
||||
"-ff",
|
||||
"-qq",
|
||||
|
|
@ -388,7 +449,11 @@ def audit_process_syscalls(metadata: dict) -> None:
|
|||
str(trace_base),
|
||||
"--",
|
||||
str(executable),
|
||||
],
|
||||
]
|
||||
if binary == "lazaret-launch-wrapper":
|
||||
command.append("--trace-self-test")
|
||||
result = subprocess.run(
|
||||
command,
|
||||
cwd=ROOT,
|
||||
check=False,
|
||||
)
|
||||
|
|
@ -415,6 +480,7 @@ def refresh_advisories() -> None:
|
|||
|
||||
def run_rust_tests() -> None:
|
||||
run(list(NEXTEST_COMMAND))
|
||||
run(list(CAPABILITY_TEST_COMMAND))
|
||||
run(list(DOCTEST_COMMAND))
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue