From e71cace91f658d6fb46bc903e8f7edf81bf98b29 Mon Sep 17 00:00:00 2001 From: karl Date: Tue, 13 Oct 2026 21:27:37 +0200 Subject: [PATCH] Guard Linux speech runtime dependency closure and relocatable lookup --- docs/LINUX-SPEECH-0.1.6.md | 25 +++++++++++++++++++++++++ tools/speech-runtime/CMakeLists.txt | 10 +++++++++- tools/speech-runtime/README.md | 5 +++++ tools/speech-runtime/build.py | 20 ++++++++++++++++++++ tools/speech-runtime/test_build.py | 21 +++++++++++++++++++++ 5 files changed, 80 insertions(+), 1 deletion(-) diff --git a/docs/LINUX-SPEECH-0.1.6.md b/docs/LINUX-SPEECH-0.1.6.md index 9bac60c..b12c354 100644 --- a/docs/LINUX-SPEECH-0.1.6.md +++ b/docs/LINUX-SPEECH-0.1.6.md @@ -26,3 +26,28 @@ Snapshot: `/home/karl/.cache/searchi-validation/linux-source-9bc7221`. This closes a client portability check only. It does not prove the Linux native speech worker, model setup, runtime packaging, recognition quality, desktop UI or GPU operation. Published Linux downloads remain version 0.1.4. + +## Native runtime preparation + +A normal auto runtime build from that immutable snapshot is running in +`/home/karl/.cache/searchi-validation/speech-auto-016`; it includes Vulkan +compilation and does not run GPU inference. Do not restart it merely because +an observation times out. Its full compilation and relocation remain unchecked. + +Inspection exposed an unverified release path: speech packaging flattened shared +libraries without checking ELF dependency closure or their build-tree lookup +paths. The builder now checks every staged ELF for origin-only RUNPATH and +declared dependencies, allowing a system Vulkan loader only for the dynamic +Vulkan module. Three builder tests passed, including rejection of build-tree +paths, missing libraries and a Vulkan dependency on the CPU worker. + +The CMake configuration now gives Linux shared targets their own `$ORIGIN` +RUNPATH; this is required because executable RUNPATH is not transitive. +Actual configuration and a separate native `ggml-base` build passed in +`speech-rpath-check-016`. `readelf` verified that the resulting library has +RUNPATH `$ORIGIN`; generated executable link flags use the same path. +This narrow library check does not establish complete runtime relocation. +The running original snapshot does not contain this correction. Its output +must not be released as corrected evidence. Next: wait for its terminal result, +inspect the complete ELF closure, then validate a corrected normal runtime +outside the build tree with `LD_LIBRARY_PATH` unset and forced CPU recognition. diff --git a/tools/speech-runtime/CMakeLists.txt b/tools/speech-runtime/CMakeLists.txt index bd5b6f8..3bb64a9 100644 --- a/tools/speech-runtime/CMakeLists.txt +++ b/tools/speech-runtime/CMakeLists.txt @@ -33,6 +33,12 @@ else() set(GGML_METAL OFF CACHE BOOL "" FORCE) endif() endif() +if(UNIX AND NOT APPLE AND NOT SEARCHI_INTERNAL_CPU) + # The staged worker and every dynamic backend live together. RUNPATH is + # non-transitive, so each shared target needs its own relocatable lookup. + set(CMAKE_INSTALL_RPATH "$ORIGIN") + set(CMAKE_BUILD_WITH_INSTALL_RPATH ON) +endif() add_subdirectory("${WHISPER_SOURCE}" whisper) target_compile_features(whisper PRIVATE cxx_std_17) add_executable(searchi-speech-worker worker.cpp) @@ -44,6 +50,8 @@ endif() if(MSVC) target_compile_options(searchi-speech-worker PRIVATE /utf-8) endif() -if(UNIX) +if(APPLE) set_target_properties(searchi-speech-worker PROPERTIES BUILD_RPATH "$ORIGIN;@loader_path" INSTALL_RPATH "$ORIGIN;@loader_path") +elseif(UNIX) + set_target_properties(searchi-speech-worker PROPERTIES BUILD_RPATH "$ORIGIN" INSTALL_RPATH "$ORIGIN") endif() diff --git a/tools/speech-runtime/README.md b/tools/speech-runtime/README.md index 5cf84e0..492897e 100644 --- a/tools/speech-runtime/README.md +++ b/tools/speech-runtime/README.md @@ -20,6 +20,11 @@ inventory files before launch and incorporates the inventory hash in its source completion fingerprint. A changed backend DLL therefore invalidates reuse. The builder compiles only; native Linux/macOS loading, full system-dependency closure, actual GPU speed/fallback and release packaging still require evidence. +Normal Linux builds reject staged ELF files with build-tree RUNPATH or undeclared +dependencies. Each shared target uses `$ORIGIN` for relocation; a system Vulkan +loader is permitted only for the dynamically loaded Vulkan backend. The declared +system C/C++ libraries remain a distribution compatibility requirement. A full +relocated recognition test is still required after compilation. Windows auto builds reuse the verified Vulkan SDK/loader provisioning and selected Visual Studio CRT bundling helpers. The isolated runtime inventories its own `vulkan-1.dll`, full release CRT set and their redistribution notices. Shared helper diff --git a/tools/speech-runtime/build.py b/tools/speech-runtime/build.py index e90beae..7c906dc 100644 --- a/tools/speech-runtime/build.py +++ b/tools/speech-runtime/build.py @@ -40,6 +40,24 @@ def sha(path): return result.hexdigest() +def verify_linux_closure(directory): + """Reject build-tree lookup and undeclared ELF dependencies before release.""" + helper = shared_helper("build_runtime.py") + directory = Path(directory) + artifacts = [directory / "searchi-speech-worker"] + sorted(directory.glob("*.so*")) + names = {path.name for path in artifacts} + for path in artifacts: + metadata = helper.elf_dynamic(path) + if metadata.get("RPATH") or metadata.get("RUNPATH") != ["$ORIGIN"]: + raise ValueError(f"Linux speech runtime requires origin-only RUNPATH: {path.name}") + allowed = names | helper.LINUX_SYSTEM_LIBRARIES + if path.name == "libggml-vulkan.so": + allowed = allowed | {"libvulkan.so.1"} + missing = set(metadata.get("NEEDED", [])) - allowed + if missing: + raise ValueError(f"Unpackaged Linux speech dependency for {path.name}: {sorted(missing)}") + + def validate_source(source): revision = subprocess.check_output(["git", "-C", str(source), "rev-parse", "HEAD"], text=True).strip() dirty = subprocess.check_output(["git", "-C", str(source), "status", "--porcelain"], text=True).strip() @@ -113,6 +131,8 @@ def main(): raise ValueError("Ambiguous speech dependency basename") shutil.copy2(path, stage / path.name) records[path.name] = {"path": path.name, "bytes": path.stat().st_size, "sha256": digest} + if args.device == "auto" and sys.platform.startswith("linux"): + verify_linux_closure(stage) manifest = {"schema": 1, "upstream_commit": UPSTREAM, "device": args.device, "distribution": args.device == "auto", "source_hashes": source_hashes, "files": list(records.values()), "native_os_gpu_checks": "unexecuted"} diff --git a/tools/speech-runtime/test_build.py b/tools/speech-runtime/test_build.py index 53ba5a0..617d4d2 100644 --- a/tools/speech-runtime/test_build.py +++ b/tools/speech-runtime/test_build.py @@ -28,6 +28,27 @@ class BuildContract(unittest.TestCase): self.assertTrue(callable(build.shared_helper("build_runtime.py").bundle_windows_crt)) self.assertTrue(callable(build.shared_helper("provision_vulkan.py").provision)) + def test_linux_closure_rejects_build_paths_and_missing_dependencies(self): + from types import SimpleNamespace + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + for name in ['searchi-speech-worker', 'libwhisper.so.1', 'libggml-vulkan.so']: + (root / name).write_bytes(b'fixture') + metadata = {'RUNPATH': ['$ORIGIN'], 'NEEDED': ['libwhisper.so.1', 'libc.so.6']} + helper = SimpleNamespace(elf_dynamic=lambda _: metadata, LINUX_SYSTEM_LIBRARIES={'libc.so.6'}) + with patch.object(build, 'shared_helper', return_value=helper): + build.verify_linux_closure(root) + metadata['RUNPATH'] = ['/tmp/build'] + with self.assertRaisesRegex(ValueError, 'origin-only'): + build.verify_linux_closure(root) + metadata['RUNPATH'] = ['$ORIGIN'] + metadata['NEEDED'] = ['libmissing.so'] + with self.assertRaisesRegex(ValueError, 'Unpackaged'): + build.verify_linux_closure(root) + metadata['NEEDED'] = ['libvulkan.so.1'] + with self.assertRaisesRegex(ValueError, 'Unpackaged'): + build.verify_linux_closure(root) + if __name__ == "__main__": unittest.main()