swayfx-mosaic/tooling/probes/step20c-egl-fx/test_probe.py

299 lines
14 KiB
Python

"""Tests offline uniquement ; aucun bwrap, aucun helper EGL/FX exécuté."""
import contextlib
import io
import json
import os
from pathlib import Path
import subprocess
import sys
import tempfile
import unittest
from unittest.mock import patch
import run
class SystemctlDurationTests(unittest.TestCase):
"""Passer la sortie CLI réelle au parseur puis à l'admission de production."""
def admission(self, runtime="12s", stop="2s"):
import test_cgroup_admission as fixtures
gate = run.gate.module
fixture = fixtures.AdmissionTests()
fixture.setUp()
self.addCleanup(fixture.doCleanups)
evidence = fixture.preflight()
fixture.cgroups.present = True
properties = {
"Id": fixtures.SPEC["unit"], "Description": fixtures.SPEC["description"],
"InvocationID": "c" * 32, "ControlGroup": fixtures.GROUP,
"Delegate": "yes", "TasksMax": "256", "KillMode": "control-group",
"RuntimeMaxUSec": runtime, "TimeoutStopUSec": stop,
}
# Literal CLI text, independently of POLICY. No systemctl is spawned.
text = "\n".join(f"{key}={value}" for key, value in properties.items()
if value is not None)
manager = gate.SystemManager(fixtures.UID, popen=fixtures.native_spawn_forbidden)
def captured(argv, deadline):
if "--user" in argv:
return text
return "\n".join(f"{key}={value}" for key, value in fixture.manager.owner.items())
with patch.object(manager, "call", side_effect=captured) as capture:
receipt = gate.admit(fixtures.SPEC, evidence, 43, fixture.proc,
fixture.cgroups, manager, 1)
self.addCleanup(os.close, receipt["pidfd"])
self.assertIn("show", capture.call_args_list[0].args[0])
return receipt
def test_rendered_durations_pass_actual_show_parser_and_admission(self):
receipt = self.admission()
self.assertEqual(receipt["properties"]["RuntimeMaxUSec"], "12s")
self.assertEqual(receipt["properties"]["TimeoutStopUSec"], "2s")
def test_infinite_absent_and_different_durations_are_refused(self):
for runtime, stop in (("infinity", "2s"), (None, "2s"), ("10s", "2s"),
("12s", "infinity"), ("12s", None), ("12s", "3s"),
("12000000", "2000000")):
with self.subTest(runtime=runtime, stop=stop):
with self.assertRaisesRegex(run.gate.module.Refusal, "propriétés/budget"):
self.admission(runtime, stop)
class ProtocolTests(unittest.TestCase):
def setUp(self):
self.temporary = tempfile.TemporaryDirectory()
self.addCleanup(self.temporary.cleanup)
self.lab = Path(self.temporary.name)
self.lab.chmod(0o700)
(self.lab / "bin").mkdir()
(self.lab / "fixture").mkdir()
(self.lab / "bin/probe").write_bytes(b"INERT-NOT-ELF")
(self.lab / "fixture/run.py").write_text("# inert\n")
(self.lab / "fixture/cgroup_gate.py").write_text("# inert gate\n")
self.files = [self.lab / "bin/probe", self.lab / "fixture/run.py",
self.lab / "fixture/cgroup_gate.py", Path("/usr/bin/python3"),
Path("/usr/bin/systemd-run"), Path("/usr/bin/systemctl"),
Path("/usr/bin/env"), Path("/usr/bin/readelf"),
Path("/usr/bin/bwrap"), Path("/usr/bin/prlimit")]
self.manifest = {
"schema": 2, "kind": "egl", "qualification_mode": "nominal",
"scope": run.gate.policy("a" * 32), "lab": str(self.lab), "argv": run.command(self.lab),
"environment": run.ENV, "supervisor_environment": run.HOST_ENV,
"files": {str(path): {"sha256": run.digest(path), "resolved": str(path.resolve())}
for path in self.files},
}
self.manifest["scope_argv"] = run.gate.sealed_scope_argv(self.lab, self.manifest)
def seal(self):
(self.lab / "manifest.json").write_text(json.dumps(self.manifest))
return run.digest(self.lab / "manifest.json")
def verify(self, seal=None):
if seal is None:
seal = self.seal()
with patch.object(run, "__file__", str(self.lab / "fixture/run.py")), \
patch.dict(os.environ, run.HOST_ENV, clear=True):
return run.verify(self.lab, seal)
def test_valid_identity(self):
self.assertEqual(self.verify()["schema"], 2)
def test_manifest_drift(self):
seal = self.seal()
(self.lab / "manifest.json").write_text("{}")
with self.assertRaises(run.Refusal):
self.verify(seal)
def test_binary_drift(self):
seal = self.seal()
(self.lab / "bin/probe").write_bytes(b"CHANGED")
with self.assertRaises(run.Refusal):
self.verify(seal)
def test_script_identity_drift(self):
seal = self.seal()
(self.lab / "fixture/run.py").write_text("# changed\n")
with self.assertRaises(run.Refusal):
self.verify(seal)
def test_scope_argv_and_policy_drift(self):
self.manifest["scope_argv"].append("--property=TasksMax=infinity")
with self.assertRaises(run.Refusal):
self.verify()
self.manifest["scope_argv"] = run.gate.sealed_scope_argv(self.lab, self.manifest)
self.manifest["scope"]["properties"] = {"TasksMax": "infinity"}
with self.assertRaises(run.Refusal):
self.verify()
def test_helper_drift_refuses_before_import(self):
seal = self.seal()
(self.lab / "fixture/cgroup_gate.py").write_text("raise AssertionError('import')")
with patch.object(run._SealedHelper, "__getattr__", side_effect=AssertionError("import")):
with self.assertRaisesRegex(run.Refusal, "helper"):
self.verify(seal)
def test_forbidden_mount_and_devices(self):
for device in ("/dev/dri", "/dev/input", "/dev/udmabuf", "/etc", "/sys", "/run/user"):
with self.subTest(device=device):
self.manifest["argv"] = run.command(self.lab) + ["--ro-bind", device, device]
with self.assertRaises(run.Refusal):
self.verify()
def test_environment_pwd_and_extra(self):
for altered in ({**run.ENV, "PWD": "/"}, {**run.ENV, "HOME": "/home/host"}):
self.manifest["environment"] = altered
with self.assertRaises(run.Refusal):
self.verify()
def test_supervisor_inherited_environment(self):
seal = self.seal()
with patch.dict(os.environ, {**run.HOST_ENV, "LD_PRELOAD": "bad"}, clear=True):
with self.assertRaises(run.Refusal):
run.verify(self.lab, seal)
def test_wrong_launcher_identity(self):
seal = self.seal()
with patch.dict(os.environ, run.HOST_ENV, clear=True):
with self.assertRaises(run.Refusal):
run.verify(self.lab, seal)
def test_private_mode(self):
self.lab.chmod(0o755)
with self.assertRaises(run.Refusal):
self.verify()
def test_budget_exclusive_no_retry(self):
run.durable(self.lab / "ATTEMPT", b"one", exclusive=True)
with self.assertRaises(FileExistsError):
run.durable(self.lab / "ATTEMPT", b"two", exclusive=True)
self.assertEqual((self.lab / "ATTEMPT").read_bytes(), b"one")
def test_default_plan_has_no_write_or_spawn(self):
before = sorted(self.lab.rglob("*"))
with patch.object(sys, "argv", ["run.py", "--lab", str(self.lab), "--manifest-sha256", "x"]), \
patch.object(run, "verify", return_value=self.manifest), \
patch.object(run, "durable", side_effect=AssertionError("write")), \
patch.object(subprocess, "Popen", side_effect=AssertionError("native boundary")), \
contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(run.main(), 0)
self.assertEqual(sorted(self.lab.rglob("*")), before)
def test_invalid_identity_stops_before_native_boundary(self):
with patch.object(sys, "argv", ["run.py", "--lab", str(self.lab), "--manifest-sha256", "bad",
"--approve-native-probe"]), \
patch.object(run.gate, "run_scope", side_effect=AssertionError("native boundary")), \
contextlib.redirect_stderr(io.StringIO()):
self.assertEqual(run.main(), 20)
self.assertFalse((self.lab / "ATTEMPT").exists())
def test_consumed_budget_refuses_passive_plan(self):
run.durable(self.lab / "ATTEMPT", b"one", exclusive=True)
with patch.object(sys, "argv", ["run.py", "--lab", str(self.lab), "--manifest-sha256", "x"]), \
patch.object(run, "verify", return_value=self.manifest), \
contextlib.redirect_stderr(io.StringIO()):
self.assertEqual(run.main(), 20)
def test_spawn_failure_consumes_budget(self):
arguments = ["run.py", "--lab", str(self.lab), "--manifest-sha256", "test", "--approve-native-probe"]
def spawn_failure(lab, manifest, reserve, journal):
reserve({"mock": True})
return {"cause": "spawn_failure", "error": "mock spawn failure", "collected": True}, [b"", b""]
with patch.object(sys, "argv", arguments), \
patch.object(run, "verify", return_value=self.manifest), \
patch.object(run.gate, "run_scope", side_effect=spawn_failure) as native, \
contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()):
self.assertEqual(run.main(), 20)
self.assertEqual(run.main(), 20)
self.assertEqual(native.call_count, 1)
self.assertEqual(json.loads((self.lab / "result.json").read_text())["cause"], "spawn_failure")
class DrainTests(unittest.TestCase):
def fake_python(self, script, **kwargs):
return run.supervise([sys.executable, "-I", "-c", script], **kwargs)
def test_exit_and_channels(self):
result, channels = self.fake_python("import os; os.write(1,b'out'); os.write(2,b'err')")
self.assertEqual(result["returncode"], 0)
self.assertTrue(result["collected"])
self.assertEqual(channels, [b"out", b"err"])
def test_nonzero_exit(self):
result, _ = self.fake_python("raise SystemExit(7)")
self.assertEqual(result["returncode"], 7)
self.assertEqual(result["cause"], "exit")
def test_exact_output_bound(self):
result, channels = self.fake_python(f"import os; os.write(1,b'x'*{run.LIMIT}); os.write(2,b'y'*{run.LIMIT})")
self.assertEqual(result["cause"], "exit")
self.assertEqual(list(map(len, channels)), [run.LIMIT, run.LIMIT])
def test_output_overflow(self):
result, channels = self.fake_python(f"import os; os.write(1,b'x'*{run.LIMIT + 1})")
self.assertEqual(result["cause"], "output_overflow")
self.assertEqual(len(channels[0]), run.LIMIT)
self.assertTrue(result["collected"])
def test_timeout_collection(self):
result, _ = self.fake_python("import time; time.sleep(30)", payload_seconds=0.1, total_seconds=0.8)
self.assertEqual(result["cause"], "timeout")
self.assertTrue(result["collected"])
self.assertLess(result["elapsed"], 1.0)
def test_closed_pipes_preserve_cleanup_reserve(self):
result, _ = self.fake_python("import os,time; os.close(1); os.close(2); time.sleep(30)",
payload_seconds=0.1, total_seconds=0.8)
self.assertEqual(result["cause"], "timeout")
self.assertTrue(result["collected"])
self.assertLess(result["elapsed"], 0.6)
def test_read_failure_collects_owned_child(self):
real_popen = subprocess.Popen
children = []
def remember(*args, **kwargs):
child = real_popen(*args, **kwargs)
children.append(child)
return child
real_read = os.read
def fail_drain(fd, size):
if size == 65536:
raise OSError("mock read failure")
return real_read(fd, size)
with patch.object(run.subprocess, "Popen", side_effect=remember), \
patch.object(run.os, "read", side_effect=fail_drain):
result, _ = self.fake_python("import os,time; os.write(1,b'x'); time.sleep(30)")
self.assertEqual(result["cause"], "supervisor_failure")
self.assertIsNotNone(children[0].returncode)
def test_collection_failure_explicit(self):
real_wait = subprocess.Popen.wait
def delayed_wait(child, *args, **kwargs):
real_wait(child, timeout=1)
raise subprocess.TimeoutExpired("mock collection", 0)
with patch.object(subprocess.Popen, "wait", delayed_wait):
result, _ = self.fake_python("pass", payload_seconds=0.05, total_seconds=0.2)
self.assertEqual(result["cause"], "cleanup_unproven")
self.assertFalse(result["collected"])
class SourceTests(unittest.TestCase):
def test_device_guard_precedes_fx_and_ownership(self):
source = Path(__file__).with_name("probe.c").read_text()
self.assertLess(source.index("query_display(display, EGL_DEVICE_EXT"),
source.index("renderer = fx_renderer_create_egl(egl)"))
self.assertLess(source.index("if (!software || drm)"), source.index("renderer = fx_renderer_create_egl(egl)"))
self.assertIn("} else if (egl != NULL)", source)
self.assertIn("egl = NULL;", source)
for forbidden in ("eglQueryDevicesEXT(", "wlr_fx_renderer_get_egl(", "wl_display_create(",
"wlr_backend_start(", "wlr_headless_add_output(", "wlr_allocator_autocreate(",
"wlr_buffer_create(", "glDraw", "->texture_caps", "->egl", "->display"):
self.assertNotIn(forbidden, source)
self.assertIn('"PWD=/tmp"', source)
self.assertIn("modifiers > 4096", source)
if __name__ == "__main__":
unittest.main()