Package portable Linux runtime and verify Google CPU search
This commit is contained in:
1 parent
e3558a0104
commit
0b56ab553c
22 files changed
+1585
-16
No files matched your search
@@ -3,6 +3,7 @@ import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
@@ -17,6 +18,87 @@ WINDOWS_CRT_FILES = (
|
||||
"msvcp140_atomic_wait.dll", "msvcp140_codecvt_ids.dll", "vccorlib140.dll",
|
||||
"vcruntime140.dll", "vcruntime140_1.dll", "vcruntime140_threads.dll",
|
||||
)
|
||||
LINUX_CPU_VARIANTS = {"x64", "sse42", "sandybridge", "ivybridge", "piledriver", "haswell",
|
||||
"skylakex", "cannonlake", "cascadelake", "icelake", "cooperlake", "zen4",
|
||||
"alderlake", "sapphirerapids"}
|
||||
LINUX_SYSTEM_LIBRARIES = {"libc.so.6", "libm.so.6", "libdl.so.2", "libpthread.so.0",
|
||||
"librt.so.1", "libstdc++.so.6", "libgcc_s.so.1", "ld-linux-x86-64.so.2"}
|
||||
|
||||
|
||||
def linux_dispatch(backend):
|
||||
"""Only normal Linux Vulkan modes use the packaged shared-module layout."""
|
||||
return (sys.platform.startswith("linux") and platform.machine().lower() in {"amd64", "x86_64"}
|
||||
and backend in {"auto", "vulkan"})
|
||||
|
||||
|
||||
def elf_dynamic(path):
|
||||
"""Read ELF metadata without executing the artifact or following ldd hooks."""
|
||||
output = subprocess.check_output(["readelf", "--dynamic", str(path)], text=True)
|
||||
values = {}
|
||||
for key, value in re.findall(r"\((NEEDED|SONAME|RUNPATH|RPATH)\).*?\[([^\]]+)\]", output):
|
||||
values.setdefault(key, []).append(value)
|
||||
return values
|
||||
|
||||
|
||||
def bundle_linux_libraries(build):
|
||||
"""Flatten approved SONAME files beside the worker and record dependency closure.
|
||||
|
||||
System libc/C++ ABI remains a declared distro compatibility gate. Vulkan's
|
||||
loader is optional: only the dynamically loaded GPU module may depend on it.
|
||||
Preflight precedes flattening; the preceding CMake build itself is not a
|
||||
transactional release replacement. Staging performs that separate swap.
|
||||
"""
|
||||
build = Path(build)
|
||||
worker = build / "searchi-gemma-worker"
|
||||
candidates = [build / "bin" / f"lib{name}.so" for name in ("ggml", "ggml-base", "llama", "mtmd")]
|
||||
candidates += [build / f"libggml-cpu-{variant}.so" for variant in sorted(LINUX_CPU_VARIANTS)]
|
||||
candidates.append(build / "libggml-vulkan.so")
|
||||
records = []
|
||||
names = set()
|
||||
for link in candidates:
|
||||
if not link.is_file():
|
||||
raise RuntimeError(f"Required Linux runtime library is missing: {link.name}")
|
||||
source = link.resolve()
|
||||
if not source.is_relative_to(build.resolve()) or not source.is_file():
|
||||
raise RuntimeError("Linux runtime library escapes its build directory")
|
||||
metadata = elf_dynamic(source)
|
||||
sonames = metadata.get("SONAME", [])
|
||||
module = link.parent == build
|
||||
if ((not module and len(sonames) != 1) or len(sonames) > 1
|
||||
or any(not re.fullmatch(r"lib[A-Za-z0-9_-]+\.so(?:\.\d+)*", name) for name in sonames)):
|
||||
raise RuntimeError("Linux runtime has invalid or missing SONAME")
|
||||
# Backend discovery uses the unversioned module filename; core linkage
|
||||
# uses its SONAME. Both destinations are actual files, never symlinks.
|
||||
name = link.name if module else sonames[0]
|
||||
if name in names:
|
||||
raise RuntimeError("Duplicate Linux runtime library")
|
||||
names.add(name)
|
||||
destination = build / name
|
||||
records.append((source, destination, metadata))
|
||||
all_names = names
|
||||
for path, metadata in [(worker, elf_dynamic(worker)), *[(destination, metadata) for _, destination, metadata in records]]:
|
||||
if metadata.get("RPATH") or metadata.get("RUNPATH") != ["$ORIGIN"]:
|
||||
raise RuntimeError(f"Linux runtime requires origin-only RUNPATH: {path.name}")
|
||||
needed = set(metadata.get("NEEDED", []))
|
||||
allowed = all_names | LINUX_SYSTEM_LIBRARIES
|
||||
if path.name == "libggml-vulkan.so":
|
||||
allowed = allowed | {"libvulkan.so.1"}
|
||||
if needed - allowed:
|
||||
raise RuntimeError(f"Unpackaged Linux runtime dependency for {path.name}: {sorted(needed - allowed)}")
|
||||
for source, destination, _ in records:
|
||||
if destination != source:
|
||||
if destination.is_symlink():
|
||||
destination.unlink()
|
||||
shutil.copy2(source, destination)
|
||||
def checksum(path):
|
||||
with path.open("rb") as stream:
|
||||
return hashlib.file_digest(stream, "sha256").hexdigest()
|
||||
inventory = {"platform": "linux", "worker_sha256": checksum(worker),
|
||||
"libraries": [{"path": path.name, "bytes": path.stat().st_size, "sha256": checksum(path)}
|
||||
for _, path, _ in sorted(records)]}
|
||||
temporary = build / "runtime-libraries.json.partial"
|
||||
temporary.write_text(json.dumps(inventory, indent=2), encoding="utf-8")
|
||||
os.replace(temporary, build / "runtime-libraries.json")
|
||||
|
||||
|
||||
def bundle_windows_crt(build):
|
||||
@@ -90,7 +172,7 @@ def main():
|
||||
reverse = subprocess.run(["git", "apply", "--reverse", "--check", str(patch)], cwd=source, capture_output=True)
|
||||
if reverse.returncode:
|
||||
raise RuntimeError("Runtime memory patch cannot be applied or verified")
|
||||
if os.name == "nt" and args.backend != "cpu":
|
||||
if (os.name == "nt" and args.backend != "cpu") or linux_dispatch(args.backend):
|
||||
for patch_name in ["cpu-dispatch.patch", "cpu-only-loader.patch"]:
|
||||
dispatch_patch = Path(__file__).with_name(patch_name)
|
||||
check = subprocess.run(["git", "apply", "--check", str(dispatch_patch)], cwd=source, capture_output=True)
|
||||
@@ -125,6 +207,8 @@ def main():
|
||||
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)
|
||||
print(f"Runtime build complete: {build}")
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
"""Prove Linux auto CPU fallback with the Vulkan loader physically absent.
|
||||
|
||||
Run as root on x86_64 Linux with util-linux unshare/mount/umount and chroot.
|
||||
This tool never escalates privileges. It launches itself inside a private mount
|
||||
namespace and verifies that namespace before changing mount propagation. The
|
||||
sealed chroot includes approved runtime files, standard C/C++ ABI libraries and
|
||||
a read-only Google model, without Vulkan loaders, ICDs, drivers or GPU devices.
|
||||
This tests missing-loader fallback, not GPU execution or distro compatibility.
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
ABI = ("libc.so.6", "libm.so.6", "libdl.so.2", "libpthread.so.0",
|
||||
"librt.so.1", "libstdc++.so.6", "libgcc_s.so.1")
|
||||
|
||||
|
||||
def require(condition, message):
|
||||
if not condition:
|
||||
raise RuntimeError(message)
|
||||
|
||||
|
||||
def checksum(path):
|
||||
with Path(path).open("rb") as stream:
|
||||
return hashlib.file_digest(stream, "sha256").hexdigest()
|
||||
|
||||
|
||||
def namespace(pid="self"):
|
||||
return Path(f"/proc/{pid}/ns/mnt").stat().st_ino
|
||||
|
||||
|
||||
def isolated_check(args, commands):
|
||||
# Check the still-live outer process as well as its recorded namespace.
|
||||
# A direct child invocation in the outer namespace fails before any mount.
|
||||
outer = namespace(args.outer_pid)
|
||||
require(outer == args.outer_namespace and namespace() != outer,
|
||||
"Private mount namespace was not established; refusing mount changes")
|
||||
subprocess.run([commands["mount"], "--make-rprivate", "/"], check=True)
|
||||
from stage_runtime import verify_runtime
|
||||
worker = args.worker.resolve()
|
||||
dependencies = verify_runtime(worker, True)
|
||||
manifest = json.loads(Path(__file__).with_name("model-manifest.json").read_text())
|
||||
artifact = manifest["files"][0]
|
||||
model = args.models.resolve() / artifact["path"]
|
||||
require(model.is_file() and model.stat().st_size == artifact["bytes"]
|
||||
and checksum(model) == artifact["sha256"], "Google model verification failed")
|
||||
base = Path(tempfile.gettempdir()).resolve()
|
||||
root = Path(tempfile.mkdtemp(prefix="searchi-loader-chroot-", dir=base)).resolve()
|
||||
require(root.parent == base and root.stat().st_uid == 0,
|
||||
"Invalid owned chroot directory")
|
||||
mounted = []
|
||||
try:
|
||||
app = root / "app"
|
||||
app.mkdir()
|
||||
for source in [worker, *dependencies]:
|
||||
destination = app / source.name
|
||||
shutil.copy2(source, destination)
|
||||
require(checksum(source) == checksum(destination), "Copied runtime checksum mismatch")
|
||||
abi_dir = root / "lib/x86_64-linux-gnu"
|
||||
abi_dir.mkdir(parents=True)
|
||||
for name in ABI:
|
||||
shutil.copy2(Path("/lib/x86_64-linux-gnu") / name, abi_dir / name)
|
||||
(root / "lib64").mkdir()
|
||||
shutil.copy2("/lib64/ld-linux-x86-64.so.2", root / "lib64/ld-linux-x86-64.so.2")
|
||||
(root / "models").mkdir()
|
||||
mounted_model = root / "models/model.gguf"
|
||||
mounted_model.touch()
|
||||
proc = root / "proc"
|
||||
proc.mkdir()
|
||||
subprocess.run([commands["mount"], "--bind", str(model), str(mounted_model)], check=True)
|
||||
mounted.append(mounted_model)
|
||||
subprocess.run([commands["mount"], "-o", "remount,bind,ro", str(mounted_model)], check=True)
|
||||
subprocess.run([commands["mount"], "-t", "proc", "-o", "ro,nosuid,nodev,noexec", "proc", str(proc)], check=True)
|
||||
mounted.append(proc)
|
||||
require(not (root / "dev").exists() and not (root / "etc").exists(),
|
||||
"Unexpected devices or driver configuration in chroot")
|
||||
require({path.name for path in abi_dir.iterdir()} == set(ABI), "Unexpected system library")
|
||||
require({path.name for path in app.iterdir()} == {worker.name, *(p.name for p in dependencies)},
|
||||
"Unexpected runtime file")
|
||||
# The worker runs with no inherited loader/ggml variables and no CPU
|
||||
# override: the absence of libvulkan must cause its actual auto fallback.
|
||||
environment = {"PATH": "/usr/bin:/bin", "LANG": "C.UTF-8"}
|
||||
process = subprocess.Popen([commands["chroot"], str(root), "/app/searchi-gemma-worker",
|
||||
"--model", "/models/model.gguf", "--threads", "1", "--device", "auto"],
|
||||
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||
text=True, encoding="utf-8", env=environment)
|
||||
try:
|
||||
stdout, stderr = process.communicate(json.dumps({"op": "code_query",
|
||||
"text": "retry failed requests with exponential backoff"}) + "\n", timeout=180)
|
||||
except BaseException:
|
||||
process.kill()
|
||||
process.wait(timeout=10)
|
||||
raise
|
||||
require(process.returncode == 0, f"Worker failed: {stderr[-4096:]}")
|
||||
lines = [json.loads(line) for line in stdout.splitlines()]
|
||||
require(len(lines) == 2, "Unexpected protocol output")
|
||||
hello, result = lines
|
||||
require(hello.get("ready") and hello.get("device") == "cpu"
|
||||
and not hello.get("vulkan_module_loaded") and result.get("ok")
|
||||
and result.get("device") == "cpu", "Expected auto fallback to CPU without Vulkan loading")
|
||||
vectors = result.get("vectors", [])
|
||||
require(len(vectors) == 1 and len(vectors[0]) == 768
|
||||
and all(isinstance(v, (float, int)) and math.isfinite(v) for v in vectors[0]),
|
||||
"Invalid real Google vector")
|
||||
norm = math.sqrt(sum(value * value for value in vectors[0]))
|
||||
require(abs(norm - 1) < .001, "Google vector is not normalized")
|
||||
evidence = {"platform": "linux", "device_requested": "auto", "forced_cpu": False,
|
||||
"gpu_execution": False, "vulkan_loader_present": False,
|
||||
"seal": {"system_abi": list(ABI), "proc_read_only": True, "model_read_only": True,
|
||||
"private_mount_namespace_verified": True, "gpu_devices": False,
|
||||
"icd_manifests": False, "environment": environment},
|
||||
"worker_sha256": checksum(worker), "model_sha256": checksum(model),
|
||||
"hello": hello, "dimensions": len(vectors[0]), "norm": norm,
|
||||
"limitations": ["Actual GPU and driver recovery untested", "Distribution ABI compatibility untested"]}
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.output.write_text(json.dumps(evidence, indent=2) + "\n", encoding="utf-8")
|
||||
print(json.dumps({"auto_without_loader": "cpu", "cpu_module": hello["cpu_backend_module"],
|
||||
"vulkan_loaded": False}))
|
||||
finally:
|
||||
failures = []
|
||||
for path in reversed(mounted):
|
||||
result = subprocess.run([commands["umount"], str(path)], check=False)
|
||||
if result.returncode:
|
||||
failures.append(str(path))
|
||||
# Never recursively remove a directory containing a live bind mount.
|
||||
require(not failures, f"Mount cleanup failed; preserved owned directory {root}")
|
||||
require(root.parent == base and root.name.startswith("searchi-loader-chroot-"),
|
||||
"Refusing cleanup outside owned temporary directory")
|
||||
shutil.rmtree(root)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--worker", type=Path, required=True)
|
||||
parser.add_argument("--models", type=Path, required=True)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
parser.add_argument("--outer-pid", type=int, help=argparse.SUPPRESS)
|
||||
parser.add_argument("--outer-namespace", type=int, help=argparse.SUPPRESS)
|
||||
args = parser.parse_args()
|
||||
require(sys.platform.startswith("linux") and platform.machine().lower() in {"amd64", "x86_64"},
|
||||
"Run this check on x86_64 Linux")
|
||||
require(os.geteuid() == 0, "Run explicitly as root; this tool does not escalate privileges")
|
||||
commands = {name: shutil.which(name) for name in ("unshare", "mount", "umount", "chroot")}
|
||||
require(all(commands.values()), "Install util-linux mount tools and chroot")
|
||||
if args.outer_pid is not None or args.outer_namespace is not None:
|
||||
require(args.outer_pid is not None and args.outer_namespace is not None,
|
||||
"Incomplete namespace handoff")
|
||||
isolated_check(args, commands)
|
||||
else:
|
||||
subprocess.run([commands["unshare"], "--mount", "--fork", sys.executable, str(Path(__file__).resolve()),
|
||||
"--worker", str(args.worker.resolve()), "--models", str(args.models.resolve()),
|
||||
"--output", str(args.output.resolve()), "--outer-pid", str(os.getpid()),
|
||||
"--outer-namespace", str(namespace())], check=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,106 @@
|
||||
"""Exercise a real Linux distribution worker on CPU, outside its build directory.
|
||||
|
||||
This checks genuine Google inference and origin-relative runtime packaging.
|
||||
It does not certify GPU operation, older processors or a Linux desktop.
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
|
||||
def sha256(path):
|
||||
with path.open("rb") as stream:
|
||||
return hashlib.file_digest(stream, "sha256").hexdigest()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--worker", type=Path, required=True)
|
||||
parser.add_argument("--models", type=Path, required=True)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
parser.add_argument("--baseline", action="store_true")
|
||||
args = parser.parse_args()
|
||||
if os.name != "posix" or not Path("/proc/self/exe").exists():
|
||||
parser.error("Run this check natively on Linux")
|
||||
# Reuse exactly the staging verifier: hashes and bounded inventory names
|
||||
# must agree with what enters a distribution.
|
||||
from stage_runtime import verify_runtime
|
||||
dependencies = verify_runtime(args.worker.resolve(), True)
|
||||
manifest = json.loads(Path(__file__).with_name("model-manifest.json").read_text())
|
||||
model = args.models.resolve() / manifest["files"][0]["path"]
|
||||
if model.stat().st_size != manifest["files"][0]["bytes"] or sha256(model) != manifest["files"][0]["sha256"]:
|
||||
raise RuntimeError("Google model checksum mismatch")
|
||||
with tempfile.TemporaryDirectory(prefix="searchi-linux-smoke-") as temporary:
|
||||
target = Path(temporary)
|
||||
copied = []
|
||||
for source in [args.worker.resolve(), *dependencies]:
|
||||
# Only exact approved CPU module names are omitted. Core libraries
|
||||
# and GPU support remain present in the baseline extraction.
|
||||
if args.baseline and source.name.startswith("libggml-cpu-") and source.name != "libggml-cpu-x64.so":
|
||||
continue
|
||||
destination = target / source.name
|
||||
shutil.copy2(source, destination)
|
||||
if sha256(destination) != sha256(source):
|
||||
raise RuntimeError("Extracted runtime checksum mismatch")
|
||||
copied.append(source.name)
|
||||
worker = target / args.worker.name
|
||||
dynamic = subprocess.check_output(["readelf", "-d", str(worker)], text=True)
|
||||
if "libvulkan" in dynamic or "libggml-vulkan" in dynamic:
|
||||
raise RuntimeError("Worker requires Vulkan at process startup; CPU fallback cannot work without the loader")
|
||||
if "$ORIGIN" not in dynamic:
|
||||
raise RuntimeError("Worker lacks origin-relative runtime lookup")
|
||||
environment = dict(os.environ, SEARCHI_FORCE_CPU="1", GGML_DISABLE_VULKAN="1")
|
||||
environment.pop("LD_LIBRARY_PATH", None)
|
||||
environment.pop("GGML_BACKEND_PATH", None)
|
||||
started = time.perf_counter()
|
||||
process = subprocess.Popen([str(worker), "--model", str(model), "--threads", "1", "--device", "cpu"],
|
||||
cwd=target, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||
text=True, encoding="utf-8", env=environment)
|
||||
try:
|
||||
stdout, stderr = process.communicate(json.dumps({"op": "code_query", "text": "retry failed requests with exponential backoff"}) + "\n", timeout=180)
|
||||
except BaseException:
|
||||
process.kill()
|
||||
process.wait(timeout=10)
|
||||
raise
|
||||
elapsed_ms = (time.perf_counter() - started) * 1000
|
||||
if process.returncode:
|
||||
raise RuntimeError(f"Worker failed ({process.returncode}): {stderr[-4096:]}")
|
||||
lines = [json.loads(line) for line in stdout.splitlines()]
|
||||
if len(lines) != 2:
|
||||
raise RuntimeError("Unexpected worker protocol output")
|
||||
hello, result = lines
|
||||
if not hello.get("ready") or hello.get("device") != "cpu" or not result.get("ok") or result.get("device") != "cpu":
|
||||
raise RuntimeError("Expected genuine ready CPU inference")
|
||||
if hello.get("vulkan_module_loaded"):
|
||||
raise RuntimeError("CPU check loaded a Vulkan provider")
|
||||
vectors = result.get("vectors")
|
||||
vector = vectors[0] if isinstance(vectors, list) and len(vectors) == 1 else None
|
||||
if not isinstance(vector, list) or len(vector) != 768 or not all(isinstance(value, (int, float)) and math.isfinite(value) for value in vector):
|
||||
raise RuntimeError("Invalid Google embedding vector")
|
||||
norm = math.sqrt(sum(value * value for value in vector))
|
||||
if abs(norm - 1) > 0.001:
|
||||
raise RuntimeError("Google vector is not normalized")
|
||||
module = hello.get("cpu_backend_module", "")
|
||||
if args.baseline and module != "libggml-cpu-x64.so":
|
||||
raise RuntimeError(f"Baseline extraction selected unexpected CPU module: {module}")
|
||||
evidence = {"platform": "linux", "cpu_only": True, "baseline_only_extraction": args.baseline,
|
||||
"worker_sha256": sha256(args.worker), "model_sha256": sha256(model),
|
||||
"files": sorted(copied), "hello": hello, "dimensions": len(vector), "norm": norm,
|
||||
"startup_query_exit_ms": elapsed_ms,
|
||||
"timing_scope": "single diagnostic smoke sample; not a performance benchmark",
|
||||
"worker_dynamic_section": dynamic,
|
||||
"limitations": ["GPU untested", "older physical CPUs untested", "desktop and installer untested", "system-loader absence not physically exercised"]}
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.output.write_text(json.dumps(evidence, indent=2) + "\n", encoding="utf-8")
|
||||
print(json.dumps({"ready": True, "cpu_module": module, "dimensions": 768, "baseline": args.baseline}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -15,6 +15,11 @@ 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"}
|
||||
|
||||
|
||||
def library_name(name):
|
||||
return isinstance(name, str) and bool(re.fullmatch(
|
||||
r"(?:[A-Za-z0-9_-]+\.dll|lib[A-Za-z0-9_-]+\.so(?:\.\d+)*)", name))
|
||||
|
||||
|
||||
def _is_link(path):
|
||||
"""Reject Windows reparse points, including junctions, on Python 3.11+."""
|
||||
path = Path(path)
|
||||
@@ -208,7 +213,7 @@ def _previous_owned(destination):
|
||||
raise RuntimeError("Invalid previous runtime inventory")
|
||||
for record in records:
|
||||
name = record.get("path") if isinstance(record, dict) else None
|
||||
if not isinstance(name, str) or not re.fullmatch(r"[A-Za-z0-9_-]+\.dll", name):
|
||||
if not library_name(name):
|
||||
raise RuntimeError("Invalid previous runtime library path")
|
||||
names.add(name)
|
||||
return names
|
||||
@@ -345,7 +350,7 @@ def verify_runtime(source, required=False):
|
||||
if not isinstance(record, dict):
|
||||
raise RuntimeError("Invalid runtime library metadata")
|
||||
name = record.get("path", "")
|
||||
if not isinstance(name, str) or not re.fullmatch(r"[A-Za-z0-9_-]+\.dll", name) or name.lower() in names:
|
||||
if not library_name(name) or name.lower() in names:
|
||||
raise RuntimeError("Invalid or duplicate runtime library path")
|
||||
names.add(name.lower())
|
||||
artifact = source.parent / name
|
||||
@@ -361,8 +366,17 @@ def verify_runtime(source, required=False):
|
||||
if hashlib.file_digest(stream, "sha256").hexdigest() != checksum:
|
||||
raise RuntimeError(f"Runtime library checksum mismatch: {name}")
|
||||
paths.append(artifact)
|
||||
if required and not ({"ggml-cpu-x64.dll", "ggml-vulkan.dll", "vulkan-1.dll", "ggml.dll", "ggml-base.dll", "llama.dll", "mtmd.dll"} | CRT_LIBRARIES).issubset(names):
|
||||
raise RuntimeError("Runtime inventory lacks required CPU/GPU libraries")
|
||||
if required:
|
||||
if (source.suffix == ".exe") == (manifest.get("platform") == "linux"):
|
||||
raise RuntimeError("Runtime inventory platform does not match worker")
|
||||
if manifest.get("platform") == "linux":
|
||||
core = {"ggml", "ggml-base", "llama", "mtmd"}
|
||||
present = {name for name in core if any(re.fullmatch(rf"lib{re.escape(name)}\.so(?:\.\d+)*", file) for file in names)}
|
||||
complete = present == core and {"libggml-cpu-x64.so", "libggml-vulkan.so"}.issubset(names)
|
||||
else:
|
||||
complete = ({"ggml-cpu-x64.dll", "ggml-vulkan.dll", "vulkan-1.dll", "ggml.dll", "ggml-base.dll", "llama.dll", "mtmd.dll"} | CRT_LIBRARIES).issubset(names)
|
||||
if not complete:
|
||||
raise RuntimeError("Runtime inventory lacks required CPU/GPU libraries")
|
||||
return [inventory, *paths]
|
||||
|
||||
|
||||
@@ -401,7 +415,8 @@ def main():
|
||||
license_path = ROOT / ".searchi/references/llama.cpp/LICENSE"
|
||||
if not license_path.is_file():
|
||||
raise RuntimeError("Native worker license is missing")
|
||||
dependencies = verify_runtime(source, os.name == "nt" and args.backend in {"auto", "vulkan"})
|
||||
required_inventory = (os.name == "nt" or os.sys.platform.startswith("linux")) and args.backend in {"auto", "vulkan"}
|
||||
dependencies = verify_runtime(source, required_inventory)
|
||||
crt_notice = source.parent / "MSVC-REDISTRIBUTION.txt"
|
||||
if os.name == "nt" and args.backend != "cpu":
|
||||
if not crt_notice.is_file():
|
||||
@@ -425,7 +440,7 @@ def main():
|
||||
sources.extend((path, f"embeddinggemma2/{path.name}") for path in artifacts)
|
||||
|
||||
def validate(candidate):
|
||||
verify_runtime(candidate / source.name, os.name == "nt" and args.backend in {"auto", "vulkan"})
|
||||
verify_runtime(candidate / source.name, required_inventory)
|
||||
if model_manifest:
|
||||
for artifact, spec in zip(artifacts, model_manifest["files"], strict=True):
|
||||
if _record(candidate / "embeddinggemma2" / artifact.name) != (spec["bytes"], spec["sha256"]):
|
||||
|
||||
@@ -1,11 +1,87 @@
|
||||
"""Redistributable preparation tests; no native builds or GPU execution."""
|
||||
from pathlib import Path
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
import build_runtime
|
||||
from build_runtime import bundle_windows_crt, WINDOWS_CRT_FILES
|
||||
|
||||
|
||||
class LinuxRuntimeTests(unittest.TestCase):
|
||||
def test_only_normal_linux_vulkan_modes_use_shared_dispatch(self):
|
||||
with patch.object(build_runtime.sys, "platform", "linux"), patch.object(build_runtime.platform, "machine", return_value="x86_64"):
|
||||
for backend in ["auto", "vulkan"]:
|
||||
self.assertTrue(build_runtime.linux_dispatch(backend))
|
||||
for backend in ["cpu", "cuda", "metal"]:
|
||||
self.assertFalse(build_runtime.linux_dispatch(backend))
|
||||
with patch.object(build_runtime.sys, "platform", "darwin"):
|
||||
self.assertFalse(build_runtime.linux_dispatch("auto"))
|
||||
with patch.object(build_runtime.sys, "platform", "linux"), patch.object(build_runtime.platform, "machine", return_value="aarch64"):
|
||||
self.assertFalse(build_runtime.linux_dispatch("auto"))
|
||||
self.assertFalse(build_runtime.linux_dispatch("vulkan"))
|
||||
|
||||
def test_readelf_parses_needed_soname_and_runpath_without_executing_worker(self):
|
||||
output = " 0x1 (NEEDED) Shared library: [libggml.so.0]\n 0xe (SONAME) Library soname: [libggml-cpu-x64.so]\n 0x1d (RUNPATH) Library runpath: [$ORIGIN]\n"
|
||||
with patch.object(build_runtime.subprocess, "check_output", return_value=output) as command:
|
||||
result = build_runtime.elf_dynamic(Path("worker"))
|
||||
self.assertEqual(result, {"NEEDED": ["libggml.so.0"], "SONAME": ["libggml-cpu-x64.so"], "RUNPATH": ["$ORIGIN"]})
|
||||
command.assert_called_once_with(["readelf", "--dynamic", "worker"], text=True)
|
||||
|
||||
def fixture(self, root):
|
||||
root = Path(root)
|
||||
(root / "bin").mkdir()
|
||||
(root / "searchi-gemma-worker").write_bytes(b"worker")
|
||||
for name in ["ggml", "ggml-base", "llama", "mtmd"]:
|
||||
(root / "bin" / f"lib{name}.so").write_bytes(name.encode())
|
||||
for variant in build_runtime.LINUX_CPU_VARIANTS:
|
||||
(root / f"libggml-cpu-{variant}.so").write_bytes(variant.encode())
|
||||
(root / "libggml-vulkan.so").write_bytes(b"vulkan")
|
||||
return root
|
||||
|
||||
def metadata(self, path):
|
||||
result = {"RUNPATH": ["$ORIGIN"], "NEEDED": ["libstdc++.so.6", "libc.so.6"]}
|
||||
if path.parent.name == "bin":
|
||||
result["SONAME"] = [path.name + ".0"]
|
||||
if path.name == "libggml-vulkan.so":
|
||||
result["NEEDED"].append("libvulkan.so.1")
|
||||
return result
|
||||
|
||||
def test_flattened_core_and_bounded_modules_have_hashed_inventory(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = self.fixture(directory)
|
||||
with patch.object(build_runtime, "elf_dynamic", side_effect=self.metadata):
|
||||
build_runtime.bundle_linux_libraries(root)
|
||||
manifest = json.loads((root / "runtime-libraries.json").read_text())
|
||||
self.assertEqual(manifest["platform"], "linux")
|
||||
self.assertEqual(len(manifest["libraries"]), 5 + len(build_runtime.LINUX_CPU_VARIANTS))
|
||||
self.assertTrue((root / "libllama.so.0").is_file())
|
||||
self.assertTrue(all(not (root / item["path"]).is_symlink() for item in manifest["libraries"]))
|
||||
self.assertTrue(all(len(item["sha256"]) == 64 for item in manifest["libraries"]))
|
||||
|
||||
def test_startup_gpu_dependency_and_build_tree_rpath_are_rejected(self):
|
||||
for problem in ["gpu", "rpath", "missing"]:
|
||||
with self.subTest(problem=problem), tempfile.TemporaryDirectory() as directory:
|
||||
root = self.fixture(directory)
|
||||
previous = root / "libllama.so.0"
|
||||
previous.write_bytes(b"previous flattened core")
|
||||
def metadata(path):
|
||||
result = self.metadata(path)
|
||||
if path.name == "searchi-gemma-worker":
|
||||
if problem == "gpu":
|
||||
result["NEEDED"].append("libvulkan.so.1")
|
||||
elif problem == "rpath":
|
||||
result["RUNPATH"] = ["/private/build"]
|
||||
else:
|
||||
result["NEEDED"].append("libunpackaged.so.9")
|
||||
return result
|
||||
with patch.object(build_runtime, "elf_dynamic", side_effect=metadata), self.assertRaises(RuntimeError):
|
||||
build_runtime.bundle_linux_libraries(root)
|
||||
self.assertFalse((root / "runtime-libraries.json").exists())
|
||||
self.assertEqual(previous.read_bytes(), b"previous flattened core")
|
||||
|
||||
|
||||
class CrtTests(unittest.TestCase):
|
||||
def fixture(self, directory):
|
||||
root = Path(directory)
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
"""Namespace guards must reject unsafe handoffs before any mount operation."""
|
||||
import argparse
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
import linux_loader_smoke
|
||||
|
||||
|
||||
class NamespaceGuardTests(unittest.TestCase):
|
||||
def test_same_namespace_refuses_mount_before_runtime_access(self):
|
||||
args = argparse.Namespace(outer_pid=123, outer_namespace=42)
|
||||
with patch.object(linux_loader_smoke, "namespace", return_value=42), \
|
||||
patch.object(linux_loader_smoke.subprocess, "run") as run:
|
||||
with self.assertRaisesRegex(RuntimeError, "Private mount namespace"):
|
||||
linux_loader_smoke.isolated_check(args, {"mount": "mount"})
|
||||
run.assert_not_called()
|
||||
|
||||
def test_changed_outer_namespace_refuses_mount(self):
|
||||
args = argparse.Namespace(outer_pid=123, outer_namespace=42)
|
||||
with patch.object(linux_loader_smoke, "namespace", return_value=43), \
|
||||
patch.object(linux_loader_smoke.subprocess, "run") as run:
|
||||
with self.assertRaisesRegex(RuntimeError, "Private mount namespace"):
|
||||
linux_loader_smoke.isolated_check(args, {"mount": "mount"})
|
||||
run.assert_not_called()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -12,6 +12,44 @@ import stage_runtime
|
||||
|
||||
|
||||
class StagingTests(unittest.TestCase):
|
||||
def test_linux_inventory_stages_core_and_modules_and_removes_previous_owned_library(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
runtime = root / "build"
|
||||
runtime.mkdir()
|
||||
worker = runtime / "searchi-gemma-worker"
|
||||
worker.write_bytes(b"ELF fixture for integrity, not executable inference")
|
||||
names = ["libggml.so.0", "libggml-base.so.0", "libllama.so.0", "libmtmd.so.0",
|
||||
"libggml-cpu-x64.so", "libggml-vulkan.so"]
|
||||
records = []
|
||||
for name in names:
|
||||
path = runtime / name
|
||||
path.write_bytes(name.encode())
|
||||
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}))
|
||||
dependencies = stage_runtime.verify_runtime(worker, True)
|
||||
destination = root / "resources"
|
||||
destination.mkdir()
|
||||
(destination / "libggml-cpu-old.so").write_bytes(b"old owned backend")
|
||||
(destination / "runtime-libraries.json").write_text(json.dumps({"libraries": [{"path": "libggml-cpu-old.so"}]}))
|
||||
(destination / "note.txt").write_bytes(b"unrelated")
|
||||
stage_runtime.stage_resources(destination, [(worker, worker.name), *[(path, path.name) for path in dependencies]],
|
||||
lambda candidate: stage_runtime.verify_runtime(candidate / worker.name, True))
|
||||
self.assertFalse((destination / "libggml-cpu-old.so").exists())
|
||||
self.assertEqual((destination / "note.txt").read_bytes(), b"unrelated")
|
||||
self.assertEqual(len(stage_runtime.verify_runtime(destination / worker.name, True)), 7)
|
||||
(destination / "libggml-vulkan.so").write_bytes(b"corrupt")
|
||||
with self.assertRaises(RuntimeError):
|
||||
stage_runtime.verify_runtime(destination / worker.name, True)
|
||||
|
||||
def test_runtime_library_names_reject_paths_and_unbounded_suffixes(self):
|
||||
for name in ["../libggml.so", "libggml.so.x", "libggml.so/1", "libggml.so..1", "ggml.dylib"]:
|
||||
with self.subTest(name=name):
|
||||
self.assertFalse(stage_runtime.library_name(name))
|
||||
self.assertTrue(stage_runtime.library_name("libggml-base.so.0.13.0"))
|
||||
|
||||
def resources(self, directory):
|
||||
root = Path(directory)
|
||||
destination = root / "src-tauri/resources"
|
||||
|
||||
Reference in new issue
Block a user