diff --git a/.github/workflows/card_stall_native_artifact.yaml b/.github/workflows/card_stall_native_artifact.yaml index 67a1148fd6..0fa3401581 100644 --- a/.github/workflows/card_stall_native_artifact.yaml +++ b/.github/workflows/card_stall_native_artifact.yaml @@ -48,11 +48,12 @@ jobs: git config remote.origin.partialclonefilter blob:none git sparse-checkout init --no-cone printf '%s\n' '/msgq_repo/' '/openpilot/selfdrive/car/card.py' '/openpilot/starpilot/card_stall_timing.py' '/openpilot/starpilot/aol/tests/test_card_stall_timing.py' '/tools/ci/card_stall_native_artifact/' '/uv.lock' '/pyproject.toml' '/.github/workflows/card_stall_native_artifact.yaml' | git sparse-checkout set --no-cone --stdin - git -c protocol.version=2 fetch --filter=blob:none --depth=4 origin "$GITHUB_SHA" + git -c protocol.version=2 fetch --filter=blob:none --depth=5 origin "$GITHUB_SHA" test "$(git rev-parse FETCH_HEAD)" = "$GITHUB_SHA" - test "$(git rev-parse FETCH_HEAD^)" = c32912dbd66aabb62b67afa793af936c547e4f94 - test "$(git rev-parse FETCH_HEAD^^)" = 402a55b49d15df5cff985d5034f0e2053a4c677a - test "$(git rev-parse FETCH_HEAD^^^)" = e8389b0b7529bad402f1f4e5fbd672ff13d9d382 + test "$(git rev-parse FETCH_HEAD^)" = c7f14368b90ff43d75909200f01923312f420504 + test "$(git rev-parse FETCH_HEAD^^)" = c32912dbd66aabb62b67afa793af936c547e4f94 + test "$(git rev-parse FETCH_HEAD^^^)" = 402a55b49d15df5cff985d5034f0e2053a4c677a + test "$(git rev-parse FETCH_HEAD^^^^)" = e8389b0b7529bad402f1f4e5fbd672ff13d9d382 git checkout --detach "$GITHUB_SHA" test ! -e openpilot/selfdrive/modeld/models - name: Verify frozen source and baseline artifacts diff --git a/tools/ci/card_stall_native_artifact/build_native.py b/tools/ci/card_stall_native_artifact/build_native.py index db3ebf0cf5..f7f15f0308 100644 --- a/tools/ci/card_stall_native_artifact/build_native.py +++ b/tools/ci/card_stall_native_artifact/build_native.py @@ -161,7 +161,9 @@ def main(): run_logged('build', [sys.executable, '-m', 'SCons', '--minimal', '--cache-disable', '-j2', 'msgq/ipc_pyx.so'], build) candidate = build/'msgq/ipc_pyx.so' - run_logged('static_abi', [sys.executable, str(HERE/'validate_elf.py'), '--baseline', str(baseline), '--candidate', str(candidate), '--report', str(artifacts/'elf_comparison.json')], output) + baseline_executable = artifacts/'baseline-pandad.elf' + baseline_executable.write_bytes(subprocess.check_output(['git', 'show', base_commit+':openpilot/selfdrive/pandad/pandad'], cwd=source)) + run_logged('static_abi', [sys.executable, str(HERE/'validate_elf.py'), '--baseline', str(baseline), '--candidate', str(candidate), '--report', str(artifacts/'elf_comparison.json'), '--baseline-executable', str(baseline_executable)], output) for name, package in [('baseline', baseline_root), ('candidate', build)]: with tempfile.TemporaryDirectory(prefix='msgq_armci_', dir='/dev/shm') as namespace: test_env = dict(env, PYTHONPATH=str(package), OPENPILOT_PREFIX=Path(namespace).name.removeprefix('msgq_')) diff --git a/tools/ci/card_stall_native_artifact/validate_elf.py b/tools/ci/card_stall_native_artifact/validate_elf.py index a0a4744b12..b2caa8d733 100644 --- a/tools/ci/card_stall_native_artifact/validate_elf.py +++ b/tools/ci/card_stall_native_artifact/validate_elf.py @@ -8,6 +8,11 @@ import re import struct import subprocess +BASELINE_EXECUTABLE_SHA256 = '03e06bcb9c70999be2031d5e36b95413287392cad3fe03aee68b3aee25ce6129' +LOADER_NAME = 'ld-linux-aarch64.so.1' +LOADER_PATH = '/lib/ld-linux-aarch64.so.1' +LOADER_ABI_SOURCE = 'https://sourceware.org/git/?p=glibc.git;a=blob;f=sysdeps/unix/sysv/linux/aarch64/ld.abilist;hb=glibc-2.34' + BASELINE_SHA256 = '5a50085f727e3b22dcf596cb42be351066b0dcf7db7866fe04be40e69fe57133' @@ -15,6 +20,89 @@ def command(*args): return subprocess.check_output(args, text=True) +def dynamic_symbols(path): + result = [] + for line in command('readelf', '--wide', '--dyn-syms', str(path)).splitlines(): + match = re.match(r'\s*\d+:\s+[0-9a-fA-F]+\s+(\d+)\s+(\w+)\s+(\w+)\s+(\w+)\s+(\w+)\s+(\S+)(?:\s+\((\d+)\))?', line) + if match: + size, kind, binding, visibility, section, name, version_index = match.groups() + result.append({'size': int(size), 'kind': kind, 'binding': binding, 'visibility': visibility, + 'section': section, 'name': name, 'version_index': int(version_index) if version_index else None}) + return result + + +def version_providers(versions): + providers = {} + provider = None + in_needs = False + for line in versions.splitlines(): + if line.startswith('Version needs section'): + in_needs = True + elif line.startswith('Version ') and 'section' in line: + in_needs = False + if not in_needs: + continue + match = re.search(r'File: (\S+)', line) + if match: + provider = match.group(1) + match = re.search(r'Name: (\S+).*?Version: (\d+)', line) + if match: + providers[int(match.group(2))] = {'provider': provider, 'version': match.group(1)} + return providers + + +def loader_exception(candidate, baseline_executable, runtime_loader): + evidence = {'status': 'rejected', 'upstream_abi_source': LOADER_ABI_SOURCE, + 'upstream_abi_entry': 'GLIBC_2.17 __stack_chk_guard D 0x8', + 'target_loader_binary_inspected': False} + errors = [] + needed_versions = {p['version'] for p in candidate['version_providers'].values() if p['provider'] == LOADER_NAME} + if needed_versions != {'GLIBC_2.17'}: + errors.append(f'loader version-needs must be exactly GLIBC_2.17: {needed_versions}') + imports = [] + for symbol in candidate['dynamic_symbols']: + provider = candidate['version_providers'].get(symbol['version_index']) + if symbol['section'] == 'UND' and provider and provider['provider'] == LOADER_NAME: + imports.append(symbol) + evidence['loader_imports'] = imports + if len(imports) != 1 or imports[0]['name'] != '__stack_chk_guard@GLIBC_2.17' or imports[0]['kind'] != 'OBJECT' or imports[0]['binding'] != 'GLOBAL': + errors.append('loader imports must be solely GLOBAL OBJECT __stack_chk_guard@GLIBC_2.17') + if baseline_executable is None or not baseline_executable.is_file(): + errors.append('exact reviewed baseline executable is required for loader exception') + else: + data = baseline_executable.read_bytes() + digest = hashlib.sha256(data).hexdigest() + evidence['baseline_executable_sha256'] = digest + if digest != BASELINE_EXECUTABLE_SHA256: + errors.append('baseline executable hash differs from reviewed pandad') + else: + interp = re.findall(r'Requesting program interpreter: ([^\]]+)', command('readelf', '--wide', '-l', str(baseline_executable))) + evidence['baseline_executable_pt_interp'] = interp + if interp != [LOADER_PATH]: + errors.append('reviewed baseline executable does not identify the exact loader path') + if not runtime_loader.is_file(): + errors.append('native CI runtime loader file is required for size/export check') + else: + data = runtime_loader.read_bytes() + if len(data) < 64 or data[:6] != b'\x7fELF\x02\x01' or struct.unpack_from('