diff --git a/backend/tests/test_delivery.py b/backend/tests/test_delivery.py index 333a337..24995c9 100644 --- a/backend/tests/test_delivery.py +++ b/backend/tests/test_delivery.py @@ -5,9 +5,11 @@ import json import os import secrets import stat +import subprocess import sys from contextlib import nullcontext from pathlib import Path +from types import SimpleNamespace from uuid import UUID import pytest @@ -43,6 +45,47 @@ node_observation = importlib.import_module("scripts.release.node_observation") installer = importlib.import_module("scripts.release.install") +@pytest.mark.parametrize("mode,owner", [(stat.S_IFREG, 0), (stat.S_IFSOCK, 1000)]) +def test_system_bus_preflight_refuses_unprotected_endpoint(monkeypatch, mode, owner): + monkeypatch.setattr(installer.preflight, "protected", lambda *args, **kwargs: None) + monkeypatch.setattr(Path, "lstat", lambda _: SimpleNamespace(st_mode=mode, st_uid=owner)) + with pytest.raises(policy.DeliveryError, match="socket absent/unprotected"): + installer.preflight.system_bus() + + +@pytest.mark.parametrize( + "response", [(0, 's "262"\n'), (0, 's "262-1-arch"\n'), (1, ""), (0, 's "other"\n')] +) +def test_system_bus_preflight_requires_bounded_manager_response(monkeypatch, response): + monkeypatch.setattr(installer.preflight, "protected", lambda *args, **kwargs: None) + monkeypatch.setattr(Path, "lstat", lambda _: SimpleNamespace(st_mode=stat.S_IFSOCK, st_uid=0)) + + def run(args, **options): + assert options["timeout"] == 5 and "--timeout=3" in args + assert options["env"] == {"PATH": "/usr/bin", "LC_ALL": "C"} + return SimpleNamespace(returncode=response[0], stdout=response[1], stderr="private detail") + + monkeypatch.setattr(installer.preflight.subprocess, "run", run) + if response[0] == 0 and response[1].startswith('s "262'): + assert installer.preflight.system_bus()["manager_reachable"] + else: + with pytest.raises(policy.DeliveryError, match="manager unavailable/incompatible") as error: + installer.preflight.system_bus() + assert "private detail" not in str(error.value) + + +def test_system_bus_timeout_is_an_explicit_preflight_refusal(monkeypatch): + monkeypatch.setattr(installer.preflight, "protected", lambda *args, **kwargs: None) + monkeypatch.setattr(Path, "lstat", lambda _: SimpleNamespace(st_mode=stat.S_IFSOCK, st_uid=0)) + + def run(args, **options): + raise subprocess.TimeoutExpired(args, options["timeout"]) + + monkeypatch.setattr(installer.preflight.subprocess, "run", run) + with pytest.raises(policy.DeliveryError, match="system message bus unavailable"): + installer.preflight.system_bus() + + @pytest.mark.parametrize("mode", [0o700, 0o755, 0o555]) def test_new_layout_directory_has_explicit_mode_under_private_umask(tmp_path, monkeypatch, mode): # Ordinary owned temporary files verify the umask regression. Root ownership diff --git a/scripts/release/preflight.py b/scripts/release/preflight.py index 6c6900e..22f078d 100644 --- a/scripts/release/preflight.py +++ b/scripts/release/preflight.py @@ -120,6 +120,40 @@ def identity_conflicts() -> list[str]: return errors +def system_bus() -> dict: + """Transient bounded operator units need the actual system message bus.""" + try: + endpoint = Path("/run/dbus/system_bus_socket") + protected(endpoint.parent, directory=True) + info = endpoint.lstat() + if not stat.S_ISSOCK(info.st_mode) or info.st_uid != 0: + raise DeliveryError("system message bus socket absent/unprotected") + protected(Path("/usr/bin/busctl")) + result = subprocess.run( + [ + "/usr/bin/busctl", + "--system", + "--timeout=3", + "get-property", + "org.freedesktop.systemd1", + "/org/freedesktop/systemd1", + "org.freedesktop.systemd1.Manager", + "Version", + ], + capture_output=True, + text=True, + timeout=5, + env={"PATH": "/usr/bin", "LC_ALL": "C"}, + ) + if result.returncode or not re.fullmatch( + r's "262(?:[-+ .][A-Za-z0-9.+() -]{1,120})?"\n?', result.stdout + ): + raise DeliveryError("system message bus manager unavailable/incompatible") + return {"manager_reachable": True, "version": result.stdout.strip()} + except (OSError, subprocess.SubprocessError) as error: + raise DeliveryError("system message bus unavailable") from error + + def observe(payload: Path, *, bundle_data: dict, require_root: bool = True) -> dict: errors = [] observations = {} @@ -135,6 +169,10 @@ def observe(payload: Path, *, bundle_data: dict, require_root: bool = True) -> d errors.append("unsupported distribution profile") if Path("/proc/1/comm").read_text().strip() != "systemd": errors.append("systemd PID1 required") + try: + observations["system_bus"] = system_bus() + except DeliveryError as error: + errors.append(str(error)) try: observations["hardware"] = hardware() flags = set(observations["hardware"]["processor"].get("flags", "").split()) @@ -258,7 +296,7 @@ def observe(payload: Path, *, bundle_data: dict, require_root: bool = True) -> d protected(Path(directory), directory=True) except (OSError, DeliveryError): errors.append("installation ancestor unprotected") - # No socket is opened for a running service and no peer is contacted. + # Port occupation checks do not connect to application peers. for port in (8000, 8100): with socket.socket() as check: try: