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
@@ -162,6 +162,11 @@ full extracted archive smoke. A legacy worker lacking `--role query` can build
|
|||||||
document vectors while failing foreground search. The archive smoke retains
|
document vectors while failing foreground search. The archive smoke retains
|
||||||
captured CLI error output when a command exits unsuccessfully.
|
captured CLI error output when a command exits unsuccessfully.
|
||||||
|
|
||||||
|
Normal runtime inventories now include build-input provenance. Rebuild the
|
||||||
|
normal runtime before staging historical caches; source changes invalidate
|
||||||
|
their inventory even if the stored file checksums are valid. See
|
||||||
|
[runtime provenance](RUNTIME-PROVENANCE.md) for scope and actual validation.
|
||||||
|
|
||||||
The optional Linux missing-loader check requires explicit root execution and
|
The optional Linux missing-loader check requires explicit root execution and
|
||||||
util-linux mount namespaces/chroot. It never escalates privileges and verifies
|
util-linux mount namespaces/chroot. It never escalates privileges and verifies
|
||||||
a private namespace before creating an isolated filesystem:
|
a private namespace before creating an isolated filesystem:
|
||||||
|
|||||||
@@ -119,6 +119,9 @@ release. Keep their evidence and unchecked production gates intact.
|
|||||||
modifying staged resources; corrupted/missing assets preserve existing worker.
|
modifying staged resources; corrupted/missing assets preserve existing worker.
|
||||||
- [x] Transactional resource staging, verified copied bytes, process locking,
|
- [x] Transactional resource staging, verified copied bytes, process locking,
|
||||||
interrupted-copy/swap recovery and backup preservation on failed rollback.
|
interrupted-copy/swap recovery and backup preservation on failed rollback.
|
||||||
|
- [x] Normal Windows/Linux source-build provenance gate before staging and
|
||||||
|
archive preparation; stale inventories preserve existing resources/packages.
|
||||||
|
Actual normal builds verified. See [runtime provenance](RUNTIME-PROVENANCE.md).
|
||||||
- [ ] Verify power-loss durability and release-tool interaction with resource
|
- [ ] Verify power-loss durability and release-tool interaction with resource
|
||||||
directory swaps on each OS; fault-injected process interruption is narrower.
|
directory swaps on each OS; fault-injected process interruption is narrower.
|
||||||
- [x] Shared monotonic Basic/Debug search metrics, granular bounded engine traces,
|
- [x] Shared monotonic Basic/Debug search metrics, granular bounded engine traces,
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# Native runtime build-input provenance
|
||||||
|
|
||||||
|
Checksum-valid historical runtime caches can be incompatible with the current
|
||||||
|
worker protocol. The Linux archive check detected this when an old worker
|
||||||
|
encoded documents but rejected the foreground `--role query` option. Source
|
||||||
|
release preparation now records and checks build-input identity before staging
|
||||||
|
or packaging a normal Windows/Linux runtime.
|
||||||
|
|
||||||
|
`runtime-libraries.json` includes `build_provenance`, schema 1, the pinned
|
||||||
|
llama.cpp commit and SHA-256 of six bounded inputs: worker.cpp, native
|
||||||
|
CMakeLists.txt, build_runtime.py and the three runtime patches. Only CRLF line
|
||||||
|
endings in these build inputs are canonicalized to LF, allowing the same
|
||||||
|
source revision to compare across checkouts. Indexed user-file bytes and their
|
||||||
|
extraction provenance are unaffected.
|
||||||
|
|
||||||
|
The builder captures provenance before CMake compilation and rejects changes
|
||||||
|
observed afterward before publishing a new inventory. Staging requires it for
|
||||||
|
normal distributions; Windows and Linux archive packagers require it as well.
|
||||||
|
Missing or stale provenance fails with an explicit normal-runtime rebuild
|
||||||
|
instruction before staged resources or an existing archive are replaced.
|
||||||
|
Internal explicit CPU tooling retains its legacy optional-inventory path.
|
||||||
|
|
||||||
|
This is a stale-cache guard, not a signature, complete upstream source audit,
|
||||||
|
proof that a binary implements its claimed protocol, or a hardware-parity test.
|
||||||
|
Real query-role and installed-layout inference checks remain necessary.
|
||||||
|
Direct Tauri commands do not replace the documented build/stage preparation.
|
||||||
|
Existing published packages and user installations do not run this source-build
|
||||||
|
check and were not altered retroactively.
|
||||||
|
|
||||||
|
## Recorded evidence, 2026-10-13
|
||||||
|
|
||||||
|
- 69 tests passed across provenance, builder, staging, Windows packaging and
|
||||||
|
Linux packaging suites; one platform-specific staging test remained skipped.
|
||||||
|
The same suites passed under optimized Python. Regressions reject a valid old
|
||||||
|
inventory without provenance, changed source and oversized inputs, while
|
||||||
|
preserving existing archives. Build-source CRLF/LF equivalence is checked.
|
||||||
|
- Actual normal Windows `auto` build and transactional staging passed.
|
||||||
|
Worker SHA-256 remains
|
||||||
|
`c01cb3ec13cfca722ee768e8350e1399a8b9a4d14a24a35adc3473a5cfc82a73`;
|
||||||
|
all 25 Windows DLL hashes also match the previously tested release exactly.
|
||||||
|
New staged inventory SHA-256:
|
||||||
|
`7fde510984b4635ce127d25ad91aab9a391300d7968ac71e4f201cc8edd052e8`.
|
||||||
|
- An actual historical extracted Windows inventory was rejected by normal
|
||||||
|
staging preflight for missing provenance. Its files were only read.
|
||||||
|
- Actual normal Linux `auto` build passed on Ubuntu 24.04 WSL. The packager
|
||||||
|
verified 21 runtime inventory/files and all six source identities against the
|
||||||
|
current repository, including checkout line-ending differences.
|
||||||
|
Worker SHA-256 remains
|
||||||
|
`b52d50a1e676bd4bf64124daff1c4ce83d86a8bde01e4eaf09838cbb24a4c28f`.
|
||||||
|
New inventory SHA-256:
|
||||||
|
`de6e06c68815b35d83c6acbde468d100413f47c74536f0d4219a9edcbeb01a86`.
|
||||||
|
|
||||||
|
These normal builds retain GPU modules plus CPU fallback; no GPU inference was
|
||||||
|
executed. macOS inventory integration, cross-compilation and actual GPU checks
|
||||||
|
remain open. Published Windows and Linux assets retain their original hashes.
|
||||||
@@ -9,10 +9,11 @@ from pathlib import Path
|
|||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
from runtime_provenance import fingerprint, UPSTREAM_COMMIT
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
TAG = "b11461"
|
TAG = "b11461"
|
||||||
COMMIT = "4d756bc72bf00a4aacf410ae15a2d315f3db400d"
|
COMMIT = UPSTREAM_COMMIT
|
||||||
WINDOWS_CRT_FILES = (
|
WINDOWS_CRT_FILES = (
|
||||||
"concrt140.dll", "msvcp140.dll", "msvcp140_1.dll", "msvcp140_2.dll",
|
"concrt140.dll", "msvcp140.dll", "msvcp140_1.dll", "msvcp140_2.dll",
|
||||||
"msvcp140_atomic_wait.dll", "msvcp140_codecvt_ids.dll", "vccorlib140.dll",
|
"msvcp140_atomic_wait.dll", "msvcp140_codecvt_ids.dll", "vccorlib140.dll",
|
||||||
@@ -40,7 +41,7 @@ def elf_dynamic(path):
|
|||||||
return values
|
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.
|
"""Flatten approved SONAME files beside the worker and record dependency closure.
|
||||||
|
|
||||||
System libc/C++ ABI remains a declared distro compatibility gate. Vulkan's
|
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:
|
with path.open("rb") as stream:
|
||||||
return hashlib.file_digest(stream, "sha256").hexdigest()
|
return hashlib.file_digest(stream, "sha256").hexdigest()
|
||||||
inventory = {"platform": "linux", "worker_sha256": checksum(worker),
|
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)}
|
"libraries": [{"path": path.name, "bytes": path.stat().st_size, "sha256": checksum(path)}
|
||||||
for _, path, _ in sorted(records)]}
|
for _, path, _ in sorted(records)]}
|
||||||
temporary = build / "runtime-libraries.json.partial"
|
temporary = build / "runtime-libraries.json.partial"
|
||||||
@@ -188,10 +190,13 @@ def main():
|
|||||||
except ImportError as error:
|
except ImportError as error:
|
||||||
raise RuntimeError("Install CMake first (python -m pip install cmake)") from error
|
raise RuntimeError("Install CMake first (python -m pip install cmake)") from error
|
||||||
worker_source = ROOT / "crates/searchi-semantic/native"
|
worker_source = ROOT / "crates/searchi-semantic/native"
|
||||||
|
provenance = fingerprint(ROOT)
|
||||||
run([cmake_exe, "-S", worker_source, "-B", build,
|
run([cmake_exe, "-S", worker_source, "-B", build,
|
||||||
"-DCMAKE_BUILD_TYPE=Release", f"-DLLAMA_CPP_DIR={source}",
|
"-DCMAKE_BUILD_TYPE=Release", f"-DLLAMA_CPP_DIR={source}",
|
||||||
f"-DSEARCHI_ACCELERATION={args.backend}"])
|
f"-DSEARCHI_ACCELERATION={args.backend}"])
|
||||||
run([cmake_exe, "--build", build, "--config", "Release", "--parallel", args.jobs])
|
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:
|
if vulkan_loader:
|
||||||
for name in ["vulkan-1.dll", "VULKAN-LICENSE.txt"]:
|
for name in ["vulkan-1.dll", "VULKAN-LICENSE.txt"]:
|
||||||
shutil.copy2(vulkan_loader / name, build / "Release" / name)
|
shutil.copy2(vulkan_loader / name, build / "Release" / name)
|
||||||
@@ -202,13 +207,14 @@ def main():
|
|||||||
with path.open("rb") as stream:
|
with path.open("rb") as stream:
|
||||||
return hashlib.file_digest(stream, "sha256").hexdigest()
|
return hashlib.file_digest(stream, "sha256").hexdigest()
|
||||||
inventory = {"worker_sha256": checksum(directory / "searchi-gemma-worker.exe"),
|
inventory = {"worker_sha256": checksum(directory / "searchi-gemma-worker.exe"),
|
||||||
|
"build_provenance": provenance,
|
||||||
"libraries": [{"path": path.name, "bytes": path.stat().st_size, "sha256": checksum(path)}
|
"libraries": [{"path": path.name, "bytes": path.stat().st_size, "sha256": checksum(path)}
|
||||||
for path in sorted(directory.glob("*.dll"))]}
|
for path in sorted(directory.glob("*.dll"))]}
|
||||||
temporary = directory / "runtime-libraries.json.partial"
|
temporary = directory / "runtime-libraries.json.partial"
|
||||||
temporary.write_text(json.dumps(inventory, indent=2), encoding="utf-8")
|
temporary.write_text(json.dumps(inventory, indent=2), encoding="utf-8")
|
||||||
os.replace(temporary, directory / "runtime-libraries.json")
|
os.replace(temporary, directory / "runtime-libraries.json")
|
||||||
if linux_dispatch(args.backend):
|
if linux_dispatch(args.backend):
|
||||||
bundle_linux_libraries(build)
|
bundle_linux_libraries(build, provenance)
|
||||||
print(f"Runtime build complete: {build}")
|
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 stat
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
|
from runtime_provenance import verify as verify_provenance
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
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"}
|
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"))
|
manifest = json.loads(inventory.read_text(encoding="utf-8"))
|
||||||
if not isinstance(manifest, dict):
|
if not isinstance(manifest, dict):
|
||||||
raise RuntimeError("Invalid runtime dependency inventory")
|
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:
|
with source.open("rb") as stream:
|
||||||
if hashlib.file_digest(stream, "sha256").hexdigest() != manifest.get("worker_sha256"):
|
if hashlib.file_digest(stream, "sha256").hexdigest() != manifest.get("worker_sha256"):
|
||||||
raise RuntimeError("Runtime worker checksum mismatch")
|
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
|
from unittest.mock import patch
|
||||||
|
|
||||||
import stage_runtime
|
import stage_runtime
|
||||||
|
import runtime_provenance
|
||||||
|
|
||||||
|
|
||||||
class StagingTests(unittest.TestCase):
|
class StagingTests(unittest.TestCase):
|
||||||
@@ -28,7 +29,7 @@ class StagingTests(unittest.TestCase):
|
|||||||
records.append({"path": name, "bytes": path.stat().st_size,
|
records.append({"path": name, "bytes": path.stat().st_size,
|
||||||
"sha256": hashlib.sha256(path.read_bytes()).hexdigest()})
|
"sha256": hashlib.sha256(path.read_bytes()).hexdigest()})
|
||||||
inventory = runtime / "runtime-libraries.json"
|
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)
|
dependencies = stage_runtime.verify_runtime(worker, True)
|
||||||
destination = root / "resources"
|
destination = root / "resources"
|
||||||
destination.mkdir()
|
destination.mkdir()
|
||||||
@@ -419,6 +420,10 @@ class StagingTests(unittest.TestCase):
|
|||||||
def test_missing_crt_notice_preserves_existing_staged_worker(self):
|
def test_missing_crt_notice_preserves_existing_staged_worker(self):
|
||||||
with tempfile.TemporaryDirectory() as directory:
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
root = Path(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 = root / ".searchi/runtime/auto/Release"
|
||||||
runtime.mkdir(parents=True)
|
runtime.mkdir(parents=True)
|
||||||
worker = runtime / "searchi-gemma-worker.exe"
|
worker = runtime / "searchi-gemma-worker.exe"
|
||||||
@@ -429,7 +434,7 @@ class StagingTests(unittest.TestCase):
|
|||||||
data = name.encode()
|
data = name.encode()
|
||||||
(runtime / name).write_bytes(data)
|
(runtime / name).write_bytes(data)
|
||||||
records.append({"path": name, "bytes": len(data), "sha256": hashlib.sha256(data).hexdigest()})
|
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 = root / ".searchi/references/llama.cpp/LICENSE"
|
||||||
license_path.parent.mkdir(parents=True)
|
license_path.parent.mkdir(parents=True)
|
||||||
license_path.write_text("license")
|
license_path.write_text("license")
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import struct
|
|||||||
import subprocess
|
import subprocess
|
||||||
import tarfile
|
import tarfile
|
||||||
import tempfile
|
import tempfile
|
||||||
|
from embeddinggemma2.runtime_provenance import verify as verify_provenance
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[1]
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
MAX_FILE = 256 * 1024 * 1024
|
MAX_FILE = 256 * 1024 * 1024
|
||||||
@@ -70,7 +71,7 @@ def dynamic(path):
|
|||||||
return values
|
return values
|
||||||
|
|
||||||
|
|
||||||
def runtime_files(runtime):
|
def runtime_files(runtime, root=ROOT):
|
||||||
inventory = runtime / "runtime-libraries.json"
|
inventory = runtime / "runtime-libraries.json"
|
||||||
regular(inventory, runtime)
|
regular(inventory, runtime)
|
||||||
if inventory.stat().st_size > 65536:
|
if inventory.stat().st_size > 65536:
|
||||||
@@ -80,6 +81,7 @@ def runtime_files(runtime):
|
|||||||
records = manifest.get("libraries") if isinstance(manifest, dict) else None
|
records = manifest.get("libraries") if isinstance(manifest, dict) else None
|
||||||
if not isinstance(manifest, dict) or manifest.get("platform") != "linux" or not isinstance(records, list) or not 1 <= len(records) <= 64:
|
if not isinstance(manifest, dict) or manifest.get("platform") != "linux" or not isinstance(records, list) or not 1 <= len(records) <= 64:
|
||||||
raise ValueError("Invalid Linux runtime inventory")
|
raise ValueError("Invalid Linux runtime inventory")
|
||||||
|
verify_provenance(manifest, root)
|
||||||
expected = {"searchi-gemma-worker": {"sha256": manifest.get("worker_sha256")}}
|
expected = {"searchi-gemma-worker": {"sha256": manifest.get("worker_sha256")}}
|
||||||
core = {}
|
core = {}
|
||||||
for record in records:
|
for record in records:
|
||||||
@@ -176,7 +178,7 @@ def package(root, executable, runtime, media, output, llama_license=None, compre
|
|||||||
root, executable, runtime, media, output = map(Path, (root, executable, runtime, media, output))
|
root, executable, runtime, media, output = map(Path, (root, executable, runtime, media, output))
|
||||||
regular(executable, executable.parent)
|
regular(executable, executable.parent)
|
||||||
elf_x64(executable)
|
elf_x64(executable)
|
||||||
expected = runtime_files(runtime)
|
expected = runtime_files(runtime, root)
|
||||||
cli_metadata = dynamic(executable)
|
cli_metadata = dynamic(executable)
|
||||||
if (cli_metadata.get("RPATH") or cli_metadata.get("RUNPATH")
|
if (cli_metadata.get("RPATH") or cli_metadata.get("RUNPATH")
|
||||||
or set(cli_metadata.get("NEEDED", [])) - SYSTEM):
|
or set(cli_metadata.get("NEEDED", [])) - SYSTEM):
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import struct
|
|||||||
import stat
|
import stat
|
||||||
import tempfile
|
import tempfile
|
||||||
import zipfile
|
import zipfile
|
||||||
|
from embeddinggemma2.runtime_provenance import verify as verify_provenance
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[1]
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
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"}
|
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"}
|
||||||
@@ -114,6 +115,7 @@ def package(root, executable, output, cli=None):
|
|||||||
manifest = json.loads(inventory_bytes)
|
manifest = json.loads(inventory_bytes)
|
||||||
if not isinstance(manifest, dict):
|
if not isinstance(manifest, dict):
|
||||||
raise ValueError("Invalid runtime dependency inventory")
|
raise ValueError("Invalid runtime dependency inventory")
|
||||||
|
verify_provenance(manifest, root)
|
||||||
libraries = manifest.get("libraries")
|
libraries = manifest.get("libraries")
|
||||||
if not isinstance(libraries, list) or not 1 <= len(libraries) <= 64:
|
if not isinstance(libraries, list) or not 1 <= len(libraries) <= 64:
|
||||||
raise ValueError("Invalid runtime dependency inventory")
|
raise ValueError("Invalid runtime dependency inventory")
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import unittest
|
|||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import package_linux as pack
|
import package_linux as pack
|
||||||
|
from embeddinggemma2 import runtime_provenance as provenance
|
||||||
|
|
||||||
|
|
||||||
def elf(path, machine=62):
|
def elf(path, machine=62):
|
||||||
@@ -22,6 +23,10 @@ def elf(path, machine=62):
|
|||||||
class LinuxPackageTests(unittest.TestCase):
|
class LinuxPackageTests(unittest.TestCase):
|
||||||
def fixture(self, directory):
|
def fixture(self, directory):
|
||||||
root = Path(directory)
|
root = Path(directory)
|
||||||
|
for name in provenance.INPUTS:
|
||||||
|
path = root / name
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_bytes((provenance.ROOT / name).read_bytes())
|
||||||
runtime, media = root / "runtime", root / "media"
|
runtime, media = root / "runtime", root / "media"
|
||||||
executable = root / "build/searchi"
|
executable = root / "build/searchi"
|
||||||
elf(executable)
|
elf(executable)
|
||||||
@@ -35,7 +40,7 @@ class LinuxPackageTests(unittest.TestCase):
|
|||||||
for name in names:
|
for name in names:
|
||||||
elf(runtime / name)
|
elf(runtime / name)
|
||||||
records.append({"path": name, "bytes": 64, "sha256": pack.digest(runtime / name)})
|
records.append({"path": name, "bytes": 64, "sha256": pack.digest(runtime / name)})
|
||||||
(runtime / "runtime-libraries.json").write_text(json.dumps({"platform": "linux", "worker_sha256": pack.digest(runtime / "searchi-gemma-worker"), "libraries": records}))
|
(runtime / "runtime-libraries.json").write_text(json.dumps({"platform": "linux", "worker_sha256": pack.digest(runtime / "searchi-gemma-worker"), "build_provenance": provenance.fingerprint(root), "libraries": records}))
|
||||||
media.mkdir()
|
media.mkdir()
|
||||||
elf(media / "bin/ffmpeg")
|
elf(media / "bin/ffmpeg")
|
||||||
elf(media / "bin/ffprobe")
|
elf(media / "bin/ffprobe")
|
||||||
@@ -115,6 +120,22 @@ class LinuxPackageTests(unittest.TestCase):
|
|||||||
compressed = {m.name: (archive.extractfile(m).read(), m.mode) for m in archive}
|
compressed = {m.name: (archive.extractfile(m).read(), m.mode) for m in archive}
|
||||||
self.assertEqual(original, compressed)
|
self.assertEqual(original, compressed)
|
||||||
|
|
||||||
|
def test_old_or_stale_build_provenance_preserves_previous_archive(self):
|
||||||
|
for mutation in ("missing", "source_changed"):
|
||||||
|
with self.subTest(mutation=mutation), tempfile.TemporaryDirectory() as directory:
|
||||||
|
fixture = self.fixture(directory)
|
||||||
|
fixture[-1].write_bytes(b"previous archive")
|
||||||
|
if mutation == "missing":
|
||||||
|
inventory = fixture[2] / "runtime-libraries.json"
|
||||||
|
manifest = json.loads(inventory.read_text())
|
||||||
|
del manifest["build_provenance"]
|
||||||
|
inventory.write_text(json.dumps(manifest))
|
||||||
|
else:
|
||||||
|
(fixture[0] / provenance.INPUTS[0]).write_bytes(b"changed worker source")
|
||||||
|
with self.assertRaisesRegex(ValueError, "provenance"):
|
||||||
|
self.run_package(fixture)
|
||||||
|
self.assertEqual(fixture[-1].read_bytes(), b"previous archive")
|
||||||
|
|
||||||
def test_media_helper_tamper_is_rejected_against_pinned_archive(self):
|
def test_media_helper_tamper_is_rejected_against_pinned_archive(self):
|
||||||
with tempfile.TemporaryDirectory() as directory:
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
fixture = self.fixture(directory)
|
fixture = self.fixture(directory)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from unittest import mock
|
|||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
from package_windows import package, CRT_LIBRARIES
|
from package_windows import package, CRT_LIBRARIES
|
||||||
|
from embeddinggemma2 import runtime_provenance as provenance
|
||||||
|
|
||||||
|
|
||||||
def executable(path, machine=0x8664):
|
def executable(path, machine=0x8664):
|
||||||
@@ -26,7 +27,7 @@ def inventory(resources):
|
|||||||
for name in ["ggml-cpu-x64.dll", "ggml-vulkan.dll", "vulkan-1.dll", "ggml.dll", "ggml-base.dll", "llama.dll", "mtmd.dll", *sorted(CRT_LIBRARIES)]:
|
for name in ["ggml-cpu-x64.dll", "ggml-vulkan.dll", "vulkan-1.dll", "ggml.dll", "ggml-base.dll", "llama.dll", "mtmd.dll", *sorted(CRT_LIBRARIES)]:
|
||||||
data = (resources / name).read_bytes()
|
data = (resources / name).read_bytes()
|
||||||
records.append({"path": name, "bytes": len(data), "sha256": hashlib.sha256(data).hexdigest()})
|
records.append({"path": name, "bytes": len(data), "sha256": hashlib.sha256(data).hexdigest()})
|
||||||
(resources / "runtime-libraries.json").write_text(json.dumps({"worker_sha256": hashlib.sha256(worker.read_bytes()).hexdigest(), "libraries": records}))
|
(resources / "runtime-libraries.json").write_text(json.dumps({"worker_sha256": hashlib.sha256(worker.read_bytes()).hexdigest(), "build_provenance": provenance.fingerprint(resources.parents[3]), "libraries": records}))
|
||||||
|
|
||||||
|
|
||||||
class PackageTests(unittest.TestCase):
|
class PackageTests(unittest.TestCase):
|
||||||
@@ -48,8 +49,24 @@ class PackageTests(unittest.TestCase):
|
|||||||
package(root, app, output, cli)
|
package(root, app, output, cli)
|
||||||
self.assertEqual(output.read_bytes(), previous)
|
self.assertEqual(output.read_bytes(), previous)
|
||||||
|
|
||||||
|
def test_old_inventory_rejected_without_replacing_archive(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
root, resources, app, output = self.fixture(directory)
|
||||||
|
output.write_bytes(b"previous archive")
|
||||||
|
inventory = resources / "runtime-libraries.json"
|
||||||
|
manifest = json.loads(inventory.read_text())
|
||||||
|
del manifest["build_provenance"]
|
||||||
|
inventory.write_text(json.dumps(manifest))
|
||||||
|
with self.assertRaisesRegex(ValueError, "provenance"):
|
||||||
|
package(root, app, output)
|
||||||
|
self.assertEqual(output.read_bytes(), b"previous archive")
|
||||||
|
|
||||||
def fixture(self, directory):
|
def fixture(self, directory):
|
||||||
root = Path(directory)
|
root = Path(directory)
|
||||||
|
for name in provenance.INPUTS:
|
||||||
|
path = root / name
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_bytes((provenance.ROOT / name).read_bytes())
|
||||||
resources = root / "apps/desktop/src-tauri/resources"
|
resources = root / "apps/desktop/src-tauri/resources"
|
||||||
app = root / "build/app.exe"
|
app = root / "build/app.exe"
|
||||||
executable(app)
|
executable(app)
|
||||||
|
|||||||
Reference in new issue
Block a user