Reject stale native runtime build provenance before release staging
This commit is contained in:
1 parent
ea2c3f4691
commit
07bbcb76e8
12 files changed
+204
-9
No files matched your search
@@ -9,10 +9,11 @@ from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from runtime_provenance import fingerprint, UPSTREAM_COMMIT
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
TAG = "b11461"
|
||||
COMMIT = "4d756bc72bf00a4aacf410ae15a2d315f3db400d"
|
||||
COMMIT = UPSTREAM_COMMIT
|
||||
WINDOWS_CRT_FILES = (
|
||||
"concrt140.dll", "msvcp140.dll", "msvcp140_1.dll", "msvcp140_2.dll",
|
||||
"msvcp140_atomic_wait.dll", "msvcp140_codecvt_ids.dll", "vccorlib140.dll",
|
||||
@@ -40,7 +41,7 @@ def elf_dynamic(path):
|
||||
return values
|
||||
|
||||
|
||||
def bundle_linux_libraries(build):
|
||||
def bundle_linux_libraries(build, provenance=None):
|
||||
"""Flatten approved SONAME files beside the worker and record dependency closure.
|
||||
|
||||
System libc/C++ ABI remains a declared distro compatibility gate. Vulkan's
|
||||
@@ -94,6 +95,7 @@ def bundle_linux_libraries(build):
|
||||
with path.open("rb") as stream:
|
||||
return hashlib.file_digest(stream, "sha256").hexdigest()
|
||||
inventory = {"platform": "linux", "worker_sha256": checksum(worker),
|
||||
"build_provenance": provenance if provenance is not None else fingerprint(ROOT),
|
||||
"libraries": [{"path": path.name, "bytes": path.stat().st_size, "sha256": checksum(path)}
|
||||
for _, path, _ in sorted(records)]}
|
||||
temporary = build / "runtime-libraries.json.partial"
|
||||
@@ -188,10 +190,13 @@ def main():
|
||||
except ImportError as error:
|
||||
raise RuntimeError("Install CMake first (python -m pip install cmake)") from error
|
||||
worker_source = ROOT / "crates/searchi-semantic/native"
|
||||
provenance = fingerprint(ROOT)
|
||||
run([cmake_exe, "-S", worker_source, "-B", build,
|
||||
"-DCMAKE_BUILD_TYPE=Release", f"-DLLAMA_CPP_DIR={source}",
|
||||
f"-DSEARCHI_ACCELERATION={args.backend}"])
|
||||
run([cmake_exe, "--build", build, "--config", "Release", "--parallel", args.jobs])
|
||||
if fingerprint(ROOT) != provenance:
|
||||
raise RuntimeError("Native build inputs changed during compilation; rerun the build")
|
||||
if vulkan_loader:
|
||||
for name in ["vulkan-1.dll", "VULKAN-LICENSE.txt"]:
|
||||
shutil.copy2(vulkan_loader / name, build / "Release" / name)
|
||||
@@ -202,13 +207,14 @@ def main():
|
||||
with path.open("rb") as stream:
|
||||
return hashlib.file_digest(stream, "sha256").hexdigest()
|
||||
inventory = {"worker_sha256": checksum(directory / "searchi-gemma-worker.exe"),
|
||||
"build_provenance": provenance,
|
||||
"libraries": [{"path": path.name, "bytes": path.stat().st_size, "sha256": checksum(path)}
|
||||
for path in sorted(directory.glob("*.dll"))]}
|
||||
temporary = directory / "runtime-libraries.json.partial"
|
||||
temporary.write_text(json.dumps(inventory, indent=2), encoding="utf-8")
|
||||
os.replace(temporary, directory / "runtime-libraries.json")
|
||||
if linux_dispatch(args.backend):
|
||||
bundle_linux_libraries(build)
|
||||
bundle_linux_libraries(build, provenance)
|
||||
print(f"Runtime build complete: {build}")
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
"""Build-input identity for detecting stale, otherwise checksum-valid runtimes."""
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
UPSTREAM_COMMIT = "4d756bc72bf00a4aacf410ae15a2d315f3db400d"
|
||||
INPUTS = (
|
||||
"crates/searchi-semantic/native/worker.cpp",
|
||||
"crates/searchi-semantic/native/CMakeLists.txt",
|
||||
"tools/embeddinggemma2/build_runtime.py",
|
||||
"tools/embeddinggemma2/no-logits.patch",
|
||||
"tools/embeddinggemma2/cpu-dispatch.patch",
|
||||
"tools/embeddinggemma2/cpu-only-loader.patch",
|
||||
)
|
||||
|
||||
|
||||
def fingerprint(root=ROOT):
|
||||
hashes = {}
|
||||
for name in INPUTS:
|
||||
path = Path(root) / name
|
||||
with path.open("rb") as source:
|
||||
data = source.read(1024 * 1024 + 1)
|
||||
if not data or len(data) > 1024 * 1024:
|
||||
raise ValueError("Native build input is empty or oversized")
|
||||
# Only build-source line endings are canonicalized; indexed user bytes
|
||||
# and extraction provenance are never processed by this helper.
|
||||
hashes[name] = hashlib.sha256(data.replace(b"\r\n", b"\n")).hexdigest()
|
||||
return {"schema": 1, "upstream_commit": UPSTREAM_COMMIT,
|
||||
"normalization": "CRLF-to-LF build inputs only", "inputs": hashes}
|
||||
|
||||
|
||||
def verify(manifest, root=ROOT):
|
||||
if not isinstance(manifest.get("build_provenance"), dict) or manifest["build_provenance"] != fingerprint(root):
|
||||
raise ValueError("Runtime build provenance missing or stale; rebuild the normal native runtime")
|
||||
@@ -10,6 +10,7 @@ import shutil
|
||||
import stat
|
||||
import time
|
||||
import uuid
|
||||
from runtime_provenance import verify as verify_provenance
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
CRT_LIBRARIES = {"concrt140.dll", "msvcp140.dll", "msvcp140_1.dll", "msvcp140_2.dll", "msvcp140_atomic_wait.dll", "msvcp140_codecvt_ids.dll", "vccorlib140.dll", "vcruntime140.dll", "vcruntime140_1.dll", "vcruntime140_threads.dll"}
|
||||
@@ -339,6 +340,11 @@ def verify_runtime(source, required=False):
|
||||
manifest = json.loads(inventory.read_text(encoding="utf-8"))
|
||||
if not isinstance(manifest, dict):
|
||||
raise RuntimeError("Invalid runtime dependency inventory")
|
||||
if required or "build_provenance" in manifest:
|
||||
try:
|
||||
verify_provenance(manifest, ROOT)
|
||||
except ValueError as error:
|
||||
raise RuntimeError(str(error)) from error
|
||||
with source.open("rb") as stream:
|
||||
if hashlib.file_digest(stream, "sha256").hexdigest() != manifest.get("worker_sha256"):
|
||||
raise RuntimeError("Runtime worker checksum mismatch")
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
import runtime_provenance as provenance
|
||||
|
||||
|
||||
class ProvenanceTests(unittest.TestCase):
|
||||
def test_line_endings_equivalent_but_changed_source_rejected(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
for name in provenance.INPUTS:
|
||||
path = root / name
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_bytes(b"first\nsecond\n")
|
||||
manifest = {"build_provenance": provenance.fingerprint(root)}
|
||||
for name in provenance.INPUTS:
|
||||
(root / name).write_bytes(b"first\r\nsecond\r\n")
|
||||
provenance.verify(manifest, root)
|
||||
(root / provenance.INPUTS[0]).write_bytes(b"different source\n")
|
||||
with self.assertRaisesRegex(ValueError, "stale"):
|
||||
provenance.verify(manifest, root)
|
||||
|
||||
def test_missing_old_inventory_rejected_and_inputs_bounded(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
for name in provenance.INPUTS:
|
||||
path = root / name
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_bytes(b"source\n")
|
||||
with self.assertRaisesRegex(ValueError, "missing or stale"):
|
||||
provenance.verify({"worker_sha256": "old-valid-hash"}, root)
|
||||
(root / provenance.INPUTS[0]).write_bytes(b"x" * (1024 * 1024 + 1))
|
||||
with self.assertRaisesRegex(ValueError, "oversized"):
|
||||
provenance.fingerprint(root)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -9,6 +9,7 @@ import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
import stage_runtime
|
||||
import runtime_provenance
|
||||
|
||||
|
||||
class StagingTests(unittest.TestCase):
|
||||
@@ -28,7 +29,7 @@ class StagingTests(unittest.TestCase):
|
||||
records.append({"path": name, "bytes": path.stat().st_size,
|
||||
"sha256": hashlib.sha256(path.read_bytes()).hexdigest()})
|
||||
inventory = runtime / "runtime-libraries.json"
|
||||
inventory.write_text(json.dumps({"platform": "linux", "worker_sha256": hashlib.sha256(worker.read_bytes()).hexdigest(), "libraries": records}))
|
||||
inventory.write_text(json.dumps({"platform": "linux", "worker_sha256": hashlib.sha256(worker.read_bytes()).hexdigest(), "build_provenance": runtime_provenance.fingerprint(), "libraries": records}))
|
||||
dependencies = stage_runtime.verify_runtime(worker, True)
|
||||
destination = root / "resources"
|
||||
destination.mkdir()
|
||||
@@ -419,6 +420,10 @@ class StagingTests(unittest.TestCase):
|
||||
def test_missing_crt_notice_preserves_existing_staged_worker(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
for name in runtime_provenance.INPUTS:
|
||||
path = root / name
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_bytes((runtime_provenance.ROOT / name).read_bytes())
|
||||
runtime = root / ".searchi/runtime/auto/Release"
|
||||
runtime.mkdir(parents=True)
|
||||
worker = runtime / "searchi-gemma-worker.exe"
|
||||
@@ -429,7 +434,7 @@ class StagingTests(unittest.TestCase):
|
||||
data = name.encode()
|
||||
(runtime / name).write_bytes(data)
|
||||
records.append({"path": name, "bytes": len(data), "sha256": hashlib.sha256(data).hexdigest()})
|
||||
(runtime / "runtime-libraries.json").write_text(json.dumps({"worker_sha256": hashlib.sha256(worker.read_bytes()).hexdigest(), "libraries": records}))
|
||||
(runtime / "runtime-libraries.json").write_text(json.dumps({"worker_sha256": hashlib.sha256(worker.read_bytes()).hexdigest(), "build_provenance": runtime_provenance.fingerprint(root), "libraries": records}))
|
||||
license_path = root / ".searchi/references/llama.cpp/LICENSE"
|
||||
license_path.parent.mkdir(parents=True)
|
||||
license_path.write_text("license")
|
||||
|
||||
Reference in new issue
Block a user