Files
searchi/tools/test_package_linux.py
T

232 lines
12 KiB
Python
Raw Normal View History

import hashlib
import io
import json
from pathlib import Path
import struct
import tarfile
import tempfile
import unittest
from unittest import mock
import package_linux as pack
def elf(path, machine=62):
path.parent.mkdir(parents=True, exist_ok=True)
data = bytearray(64)
data[:7] = b"\x7fELF\x02\x01\x01"
struct.pack_into("<HH", data, 16, 3, machine)
path.write_bytes(data)
class LinuxPackageTests(unittest.TestCase):
def fixture(self, directory):
root = Path(directory)
runtime, media = root / "runtime", root / "media"
executable = root / "build/searchi"
elf(executable)
(root / "LICENSE").write_text("application MIT notice")
license = root / ".searchi/references/llama.cpp/LICENSE"
license.parent.mkdir(parents=True)
license.write_text("llama MIT notice")
names = ["libggml.so.0", "libggml-base.so.0", "libllama.so.0", "libmtmd.so.0", "libggml-cpu-x64.so", "libggml-vulkan.so"]
elf(runtime / "searchi-gemma-worker")
records = []
for name in names:
elf(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}))
media.mkdir()
elf(media / "bin/ffmpeg")
elf(media / "bin/ffprobe")
notice = media / "licenses/upstream/LICENSE.txt"
notice.parent.mkdir(parents=True)
notice.write_text("LGPL notice")
archive = media / "verified.tar.xz"
with tarfile.open(archive, "w:xz") as output:
for name, path in [("upstream/bin/ffmpeg", media / "bin/ffmpeg"), ("upstream/bin/ffprobe", media / "bin/ffprobe"), ("upstream/LICENSE.txt", notice)]:
output.add(path, arcname=name)
manifest = root / "tools/media-runtime/manifest.json"
manifest.parent.mkdir(parents=True)
manifest.write_text(json.dumps({"provider": "test-fixture", "release": "fixture", "platforms": {"linux-x86_64": {"archive": archive.name, "bytes": archive.stat().st_size, "sha256": pack.digest(archive)}}}))
return root, executable, runtime, media, root / "candidate.tar.gz"
@staticmethod
def metadata(path):
if path.name == "searchi-gemma-worker" or path.name.startswith("lib"):
metadata = {"RUNPATH": ["$ORIGIN"], "NEEDED": ["libc.so.6"]}
if path.name in {"libggml.so.0", "libggml-base.so.0", "libllama.so.0", "libmtmd.so.0"}:
metadata["SONAME"] = [path.name]
if path.name == "libggml-vulkan.so":
metadata["NEEDED"].append("libvulkan.so.1")
return metadata
return {"NEEDED": ["libc.so.6"]}
def run_package(self, fixture):
with mock.patch.object(pack, "dynamic", side_effect=self.metadata):
return pack.package(*fixture)
def test_archive_integrity_modes_no_userdata_and_reproducibility(self):
with tempfile.TemporaryDirectory() as directory:
fixture = self.fixture(directory)
root, executable, runtime, media, output = fixture
(runtime / "settings.json").write_text("private")
(media / "index.sqlite").write_text("private")
self.run_package(fixture)
first = output.read_bytes()
self.run_package(fixture)
self.assertEqual(first, output.read_bytes())
with tarfile.open(output) as archive:
self.assertNotIn("settings.json", archive.getnames())
manifest = json.load(archive.extractfile("PACKAGE.json"))
for record in manifest["files"]:
member = archive.getmember(record["path"])
data = archive.extractfile(member).read()
self.assertEqual(record["sha256"], hashlib.sha256(data).hexdigest())
self.assertEqual(member.uid, 0)
self.assertEqual(member.mtime, 0)
self.assertEqual(archive.getmember("searchi").mode, 0o755)
self.assertEqual(archive.getmember("media-runtime/bin/ffmpeg").mode, 0o755)
self.assertEqual(archive.getmember("LICENSE.txt").mode, 0o644)
def test_tampered_runtime_preserves_previous_archive(self):
with tempfile.TemporaryDirectory() as directory:
fixture = self.fixture(directory)
self.run_package(fixture)
previous = fixture[-1].read_bytes()
(fixture[2] / "libggml-cpu-x64.so").write_bytes(b"tampered")
with self.assertRaisesRegex(ValueError, "integrity mismatch"):
self.run_package(fixture)
self.assertEqual(previous, fixture[-1].read_bytes())
def test_media_helper_tamper_is_rejected_against_pinned_archive(self):
with tempfile.TemporaryDirectory() as directory:
fixture = self.fixture(directory)
(fixture[3] / "bin/ffmpeg").write_bytes(b"tampered")
with self.assertRaisesRegex(ValueError, "differs from pinned archive"):
self.run_package(fixture)
def test_media_archive_provenance_tamper_rejected(self):
with tempfile.TemporaryDirectory() as directory:
fixture = self.fixture(directory)
with (fixture[3] / "verified.tar.xz").open("ab") as stream:
stream.write(b"tampered")
with self.assertRaisesRegex(ValueError, "provenance mismatch"):
self.run_package(fixture)
def test_mixed_architecture_rejected(self):
with tempfile.TemporaryDirectory() as directory:
fixture = self.fixture(directory)
elf(fixture[1], machine=183)
with self.assertRaisesRegex(ValueError, "x86-64 ELF"):
self.run_package(fixture)
def test_inventory_traversal_rejected(self):
with tempfile.TemporaryDirectory() as directory:
fixture = self.fixture(directory)
path = fixture[2] / "runtime-libraries.json"
manifest = json.loads(path.read_text())
manifest["libraries"][0]["path"] = "../private.so"
path.write_text(json.dumps(manifest))
with self.assertRaisesRegex(ValueError, "Unsafe package path"):
self.run_package(fixture)
def test_missing_baseline_cpu_rejected(self):
with tempfile.TemporaryDirectory() as directory:
fixture = self.fixture(directory)
path = fixture[2] / "runtime-libraries.json"
manifest = json.loads(path.read_text())
manifest["libraries"] = [r for r in manifest["libraries"] if r["path"] != "libggml-cpu-x64.so"]
path.write_text(json.dumps(manifest))
with self.assertRaisesRegex(ValueError, "lacks baseline"):
self.run_package(fixture)
def test_worker_cannot_link_vulkan_loader(self):
with tempfile.TemporaryDirectory() as directory:
fixture = self.fixture(directory)
def metadata(path):
result = self.metadata(path)
if path.name == "searchi-gemma-worker":
result["NEEDED"].append("libvulkan.so.1")
return result
with mock.patch.object(pack, "dynamic", side_effect=metadata), self.assertRaisesRegex(ValueError, "Unpackaged runtime"):
pack.package(*fixture)
def test_external_runtime_search_path_rejected(self):
with tempfile.TemporaryDirectory() as directory:
fixture = self.fixture(directory)
with mock.patch.object(pack, "dynamic", return_value={"RUNPATH": ["/build/lib"]}), self.assertRaisesRegex(ValueError, "origin-only"):
pack.package(*fixture)
def test_glibc_vector_math_is_declared_but_unknown_cli_dependency_rejected(self):
with tempfile.TemporaryDirectory() as directory:
fixture = self.fixture(directory)
def vector_math(path):
result = self.metadata(path)
if path.name == "searchi":
result["NEEDED"].append("libmvec.so.1")
return result
with mock.patch.object(pack, "dynamic", side_effect=vector_math):
pack.package(*fixture)
with tarfile.open(fixture[-1]) as archive:
manifest = json.load(archive.extractfile("PACKAGE.json"))
self.assertIn("libmvec.so.1", manifest["system_libraries"])
previous = fixture[-1].read_bytes()
def unknown(path):
result = vector_math(path)
if path.name == "searchi":
result["NEEDED"].append("libunpackaged.so.1")
return result
with mock.patch.object(pack, "dynamic", side_effect=unknown), self.assertRaisesRegex(ValueError, "unexpected native dependency"):
pack.package(*fixture)
self.assertEqual(previous, fixture[-1].read_bytes())
def test_readelf_parser_is_nonexecuting_and_preserves_dependencies(self):
with mock.patch.object(pack.subprocess, "check_output", return_value="(NEEDED) Shared library: [libc.so.6]\n(RUNPATH) Library runpath: [$ORIGIN]\n") as process:
self.assertEqual(pack.dynamic(Path("candidate")), {"NEEDED": ["libc.so.6"], "RUNPATH": ["$ORIGIN"]})
self.assertEqual(process.call_args.args[0], ["readelf", "--dynamic", "candidate"])
def test_missing_pinned_media_notice_rejected(self):
with tempfile.TemporaryDirectory() as directory:
fixture = self.fixture(directory)
(fixture[3] / "licenses/upstream/LICENSE.txt").unlink()
with self.assertRaisesRegex(ValueError, "Missing or oversized"):
self.run_package(fixture)
def test_output_cannot_overwrite_cli(self):
with tempfile.TemporaryDirectory() as directory:
root, executable, runtime, media, _ = self.fixture(directory)
original = executable.read_bytes()
with self.assertRaisesRegex(ValueError, "overwrite an input"):
self.run_package((root, executable, runtime, media, executable))
self.assertEqual(original, executable.read_bytes())
def test_changed_source_during_packaging_preserves_previous_archive(self):
with tempfile.TemporaryDirectory() as directory:
fixture = self.fixture(directory)
self.run_package(fixture)
previous = fixture[-1].read_bytes()
original_add = pack.add
def mutate(archive, name, data, mode):
original_add(archive, name, data, mode)
if name == "LICENSE.txt":
fixture[1].write_bytes(b"changed after preflight")
with mock.patch.object(pack, "add", side_effect=mutate), self.assertRaisesRegex(ValueError, "Source changed"):
self.run_package(fixture)
self.assertEqual(previous, fixture[-1].read_bytes())
def test_unapproved_plugin_rejected_even_with_valid_hash(self):
with tempfile.TemporaryDirectory() as directory:
fixture = self.fixture(directory)
runtime = fixture[2]
elf(runtime / "libarbitrary.so")
inventory = runtime / "runtime-libraries.json"
manifest = json.loads(inventory.read_text())
manifest["libraries"].append({"path": "libarbitrary.so", "bytes": 64, "sha256": pack.digest(runtime / "libarbitrary.so")})
inventory.write_text(json.dumps(manifest))
with self.assertRaisesRegex(ValueError, "Unapproved runtime library"):
self.run_package(fixture)
if __name__ == "__main__":
unittest.main()