Require ELF closure verification when staging Linux speech runtime
This commit is contained in:
1 parent
e71cace91f
commit
61afa08b8b
3 files changed
+84
No files matched your search
@@ -51,3 +51,16 @@ The running original snapshot does not contain this correction. Its output
|
|||||||
must not be released as corrected evidence. Next: wait for its terminal result,
|
must not be released as corrected evidence. Next: wait for its terminal result,
|
||||||
inspect the complete ELF closure, then validate a corrected normal runtime
|
inspect the complete ELF closure, then validate a corrected normal runtime
|
||||||
outside the build tree with `LD_LIBRARY_PATH` unset and forced CPU recognition.
|
outside the build tree with `LD_LIBRARY_PATH` unset and forced CPU recognition.
|
||||||
|
|
||||||
|
Linux staging now invokes the same ELF closure verification after manifest,
|
||||||
|
architecture and CPU/acceleration checks. A valid inventory cannot bypass this
|
||||||
|
check; missing `readelf` also fails staging. Six targeted staging tests passed,
|
||||||
|
including inspected ELF coverage, failure propagation and Windows independence.
|
||||||
|
|
||||||
|
The upcoming real CPU test fixture was prepared from the unchanged verified
|
||||||
|
English WAV (source SHA-256
|
||||||
|
`0aa1fa97d367d987bfb3bb01d7eca11db77a544552ccb8dc0a78ad74d0b496ea`).
|
||||||
|
Exact mono 16-kHz signed PCM16 samples were converted to float32 by division by
|
||||||
|
32768, preserving all 360192 samples. PCM SHA-256:
|
||||||
|
`56948226ac5a0ad4b70523a856dad0851929c7067957f339e9a5b30fc9b369ce`.
|
||||||
|
Fixture preparation succeeded; native recognition has not yet been executed.
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
"""Verify and atomically stage the isolated normal speech runtime; no inference."""
|
"""Verify and atomically stage the isolated normal speech runtime; no inference."""
|
||||||
import argparse
|
import argparse
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import importlib.util
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import re
|
import re
|
||||||
@@ -28,6 +29,15 @@ def input_path(root, name):
|
|||||||
return Path(root) / 'tools' / (name if '/' in name else 'speech-runtime/' + name)
|
return Path(root) / 'tools' / (name if '/' in name else 'speech-runtime/' + name)
|
||||||
|
|
||||||
|
|
||||||
|
def verify_linux_closure(directory):
|
||||||
|
# Load our pinned builder, never code from the candidate runtime directory.
|
||||||
|
path = ROOT / 'tools/speech-runtime/build.py'
|
||||||
|
spec = importlib.util.spec_from_file_location('searchi_speech_build', path)
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
module.verify_linux_closure(directory)
|
||||||
|
|
||||||
|
|
||||||
def inventory(directory):
|
def inventory(directory):
|
||||||
path = Path(directory) / 'runtime-manifest.json'
|
path = Path(directory) / 'runtime-manifest.json'
|
||||||
if path.is_symlink() or not path.is_file() or not 0 < path.stat().st_size <= 512 * 1024:
|
if path.is_symlink() or not path.is_file() or not 0 < path.stat().st_size <= 512 * 1024:
|
||||||
@@ -84,6 +94,8 @@ def verify_directory(directory, root=ROOT, platform='windows', architecture='x86
|
|||||||
'VULKAN-LICENSE.txt', 'MSVC-REDISTRIBUTION.txt'} | CRT_LIBRARIES
|
'VULKAN-LICENSE.txt', 'MSVC-REDISTRIBUTION.txt'} | CRT_LIBRARIES
|
||||||
if platform == 'windows' and not windows_libraries.issubset(names):
|
if platform == 'windows' and not windows_libraries.issubset(names):
|
||||||
raise ValueError('Speech runtime lacks required shared libraries')
|
raise ValueError('Speech runtime lacks required shared libraries')
|
||||||
|
if platform == 'linux':
|
||||||
|
verify_linux_closure(directory)
|
||||||
records = {'speech-runtime/' + record['path']: record for record in manifest['files']}
|
records = {'speech-runtime/' + record['path']: record for record in manifest['files']}
|
||||||
manifest_path = directory / 'runtime-manifest.json'
|
manifest_path = directory / 'runtime-manifest.json'
|
||||||
records['speech-runtime/runtime-manifest.json'] = {
|
records['speech-runtime/runtime-manifest.json'] = {
|
||||||
|
|||||||
@@ -3,12 +3,71 @@ from pathlib import Path
|
|||||||
import shutil
|
import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
import stage_speech_worker as speech
|
import stage_speech_worker as speech
|
||||||
import test_package_windows as windows
|
import test_package_windows as windows
|
||||||
|
|
||||||
|
|
||||||
class SpeechStagingTests(unittest.TestCase):
|
class SpeechStagingTests(unittest.TestCase):
|
||||||
|
def linux_fixture(self, directory):
|
||||||
|
root, resources, _, _ = windows.PackageTests().fixture(directory)
|
||||||
|
runtime = resources / 'speech-runtime'
|
||||||
|
manifest_path = runtime / 'runtime-manifest.json'
|
||||||
|
manifest = json.loads(manifest_path.read_text())
|
||||||
|
for record in manifest['files']:
|
||||||
|
(runtime / record['path']).unlink()
|
||||||
|
manifest['files'] = []
|
||||||
|
for name in ('searchi-speech-worker', 'libggml-cpu-x64.so', 'libggml-vulkan.so'):
|
||||||
|
path = runtime / name
|
||||||
|
path.write_bytes(b'ELF fixture inspected by mocked readelf')
|
||||||
|
manifest['files'].append({'path': name, 'bytes': path.stat().st_size,
|
||||||
|
'sha256': speech.digest(path)})
|
||||||
|
manifest_path.write_text(json.dumps(manifest))
|
||||||
|
return root, runtime
|
||||||
|
|
||||||
|
def test_linux_hash_valid_inventory_requires_elf_closure(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
root, runtime = self.linux_fixture(directory)
|
||||||
|
with mock.patch.object(speech, 'identity', return_value=('linux', 'x86_64', None)), \
|
||||||
|
mock.patch.object(speech, 'verify_linux_closure') as closure:
|
||||||
|
records = speech.verify_directory(runtime, root, platform='linux')
|
||||||
|
closure.assert_called_once_with(runtime)
|
||||||
|
self.assertIn('speech-runtime/searchi-speech-worker', records)
|
||||||
|
|
||||||
|
def test_linux_closure_failure_and_missing_readelf_are_not_bypassed(self):
|
||||||
|
for error in (ValueError('Unpackaged Linux speech dependency'),
|
||||||
|
FileNotFoundError('readelf is required')):
|
||||||
|
with self.subTest(error=type(error)), tempfile.TemporaryDirectory() as directory:
|
||||||
|
root, runtime = self.linux_fixture(directory)
|
||||||
|
with mock.patch.object(speech, 'identity', return_value=('linux', 'x86_64', None)), \
|
||||||
|
mock.patch.object(speech, 'verify_linux_closure', side_effect=error):
|
||||||
|
with self.assertRaises(type(error)):
|
||||||
|
speech.verify_directory(runtime, root, platform='linux')
|
||||||
|
|
||||||
|
def test_windows_does_not_require_elf_tools(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
root, resources, _, _ = windows.PackageTests().fixture(directory)
|
||||||
|
with mock.patch.object(speech, 'verify_linux_closure') as closure:
|
||||||
|
speech.verify_directory(resources / 'speech-runtime', root)
|
||||||
|
closure.assert_not_called()
|
||||||
|
|
||||||
|
def test_loaded_builder_checks_every_linux_elf_and_rejects_build_tree_runpath(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
_, runtime = self.linux_fixture(directory)
|
||||||
|
valid = '(RUNPATH) Library runpath: [$ORIGIN]\n(NEEDED) Shared library: [libc.so.6]\n'
|
||||||
|
with mock.patch('subprocess.check_output', return_value=valid) as readelf:
|
||||||
|
speech.verify_linux_closure(runtime)
|
||||||
|
self.assertEqual(readelf.call_count, 3)
|
||||||
|
self.assertEqual({call.args[0][2] for call in readelf.call_args_list},
|
||||||
|
{str(runtime / name) for name in
|
||||||
|
('searchi-speech-worker', 'libggml-cpu-x64.so', 'libggml-vulkan.so')})
|
||||||
|
self.assertTrue(all(call.args[0][:2] == ['readelf', '--dynamic']
|
||||||
|
for call in readelf.call_args_list))
|
||||||
|
with mock.patch('subprocess.check_output', return_value=valid.replace('$ORIGIN', '/build/whisper')):
|
||||||
|
with self.assertRaisesRegex(ValueError, 'origin-only RUNPATH'):
|
||||||
|
speech.verify_linux_closure(runtime)
|
||||||
|
|
||||||
def test_replacement_prunes_obsolete_declared_code_and_keeps_closed_inventory(self):
|
def test_replacement_prunes_obsolete_declared_code_and_keeps_closed_inventory(self):
|
||||||
with tempfile.TemporaryDirectory() as directory:
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
root, resources, _, _ = windows.PackageTests().fixture(directory)
|
root, resources, _, _ = windows.PackageTests().fixture(directory)
|
||||||
|
|||||||
Reference in new issue
Block a user