From fdcf1db29073f2a9d200b668903897437537744c Mon Sep 17 00:00:00 2001 From: Jeffrey Wong Date: Tue, 20 Oct 2026 17:00:04 -0500 Subject: [PATCH] Build: keep the hash-pinned model guard libraries instead of recompiling them --- .../models/runtime_bridge/SConscript | 23 +++++- .../models/tests/test_runtime_bridge_build.py | 76 +++++++++++++++++++ prebuilt.json | 2 +- 3 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 openpilot/starpilot/models/tests/test_runtime_bridge_build.py diff --git a/openpilot/starpilot/models/runtime_bridge/SConscript b/openpilot/starpilot/models/runtime_bridge/SConscript index e92eda4c50..c71d1daad6 100644 --- a/openpilot/starpilot/models/runtime_bridge/SConscript +++ b/openpilot/starpilot/models/runtime_bridge/SConscript @@ -37,6 +37,25 @@ if arch == 'comma_arm64': SHCCFLAGS=['$CCFLAGS', '-fPIC'], SHCFLAGS=['$CFLAGS'], SHLINKFLAGS=['$LINKFLAGS', '-shared'], ) - guard_env.SharedLibrary('bounded_wait', ['bounded_wait.c']) - guard_env.SharedLibrary('usb_guards', ['usb_guards.c']) + # guard-policy.json pins both libraries by SHA-256 and the tree ships them built. A device build that recompiles + # them writes different bytes over the tracked files, and the guard then stops modeld, dmonitoringmodeld and + # controlsd at import ("Model submission guard source/artifact changed"). Keep the shipped libraries while they + # match the policy; rebuild only when they do not, which the guard then refuses at runtime. + import hashlib + import json + import os + here = Dir('.').srcnode().abspath + with open(os.path.join(here, 'guard-policy.json'), 'rb') as policy_file: + pinned = json.load(policy_file)['files'] + + def shipped(name): + path = os.path.join(here, name) + if os.path.islink(path) or not os.path.isfile(path): + return False + with open(path, 'rb') as library: + return hashlib.sha256(library.read()).hexdigest() == pinned.get(name) + + if not (shipped('libbounded_wait.so') and shipped('libusb_guards.so')): + guard_env.SharedLibrary('bounded_wait', ['bounded_wait.c']) + guard_env.SharedLibrary('usb_guards', ['usb_guards.c']) Return('guard_env') diff --git a/openpilot/starpilot/models/tests/test_runtime_bridge_build.py b/openpilot/starpilot/models/tests/test_runtime_bridge_build.py new file mode 100644 index 0000000000..fcdb421afc --- /dev/null +++ b/openpilot/starpilot/models/tests/test_runtime_bridge_build.py @@ -0,0 +1,76 @@ +"""The guard libraries are pinned by hash; a device build must not overwrite them. Fake SCons nodes, no compilers.""" + +import hashlib +import json +import shutil +import sys +from pathlib import Path +from types import ModuleType, SimpleNamespace +from unittest.mock import Mock + +import pytest + +BRIDGE = Path(__file__).resolve().parents[1] / "runtime_bridge" +SOURCE = BRIDGE / "SConscript" +LIBRARIES = ("libbounded_wait.so", "libusb_guards.so") + + +class Returned(Exception): + pass + + +def evaluate(directory, arch="comma_arm64"): + env = Mock() + env.subst_list.return_value = [] + guard_env = env.Clone.return_value + + def stop(*_): + raise Returned() + + scope = {"env": env, "arch": arch, "Import": Mock(), "Return": stop, + "Dir": lambda _: SimpleNamespace(srcnode=lambda: SimpleNamespace(abspath=str(directory)))} + try: + exec(compile(SOURCE.read_text(), str(SOURCE), "exec"), scope) + except Returned: + pass + return guard_env + + +@pytest.fixture(autouse=True) +def fake_scons(monkeypatch): + errors = ModuleType("SCons.Errors") + errors.UserError = type("UserError", (Exception,), {}) + monkeypatch.setitem(sys.modules, "SCons", ModuleType("SCons")) + monkeypatch.setitem(sys.modules, "SCons.Errors", errors) + + +@pytest.fixture +def bridge(tmp_path): + for name in ("guard-policy.json", "bounded_wait.c", "usb_guards.c", *LIBRARIES): + shutil.copy(BRIDGE / name, tmp_path / name) + return tmp_path + + +def test_shipped_libraries_match_the_policy_that_pins_them(): + pinned = json.loads((BRIDGE / "guard-policy.json").read_bytes())["files"] + for name in LIBRARIES: + assert hashlib.sha256((BRIDGE / name).read_bytes()).hexdigest() == pinned[name] + + +def test_matching_shipped_libraries_are_kept_not_rebuilt(bridge): + assert not evaluate(bridge).SharedLibrary.called + + +@pytest.mark.parametrize("damage", ("changed", "missing")) +@pytest.mark.parametrize("name", LIBRARIES) +def test_a_changed_or_missing_library_is_rebuilt_so_the_guard_can_refuse_it(bridge, name, damage): + if damage == "changed": + (bridge / name).write_bytes((bridge / name).read_bytes() + b"\0") + else: + (bridge / name).unlink() + built = [call.args[0] for call in evaluate(bridge).SharedLibrary.call_args_list] + assert built == ["bounded_wait", "usb_guards"] + + +def test_other_architectures_build_nothing(bridge): + assert not evaluate(bridge, arch="x86_64").SharedLibrary.called diff --git a/prebuilt.json b/prebuilt.json index 03e08c28b9..c3179870c1 100644 --- a/prebuilt.json +++ b/prebuilt.json @@ -1 +1 @@ -{"protected_inventory_sha256": "71d6cd3d1f42bd080c70cfabdbe93c972864379046240463577c088502bb51fc", "schema_version": 1} +{"protected_inventory_sha256": "6013e0dc92e1a20be5b399fbd91223b040722469e22c0bfb9d79dd65ea0956f0", "schema_version": 1}