Add fail-closed Linux process and descriptor primitives

This commit is contained in:
ms2k 2026-07-14 22:08:29 +09:00
commit 1468f6b841

View file

@ -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))