For every built executable, internal shared library, and plugin module, verify two link-time properties via readelf: 1. No imported GLIBC symbol's version is newer than 2.34. 2. The dynamic section's NEEDED entries reference only glibc, the runtime linker, our own libraries.
188 lines
6.1 KiB
Python
Executable File
188 lines
6.1 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: LGPL-2.1-or-later
|
|
"""Verify two ABI properties of the supplied ELF objects:
|
|
|
|
1. No imported GLIBC symbol is newer than the baseline (default 2.34).
|
|
2. Each binary's NEEDED entries only reference glibc itself or our own
|
|
internal shared libraries — anything else means we've grown a hard
|
|
link-time dependency on a third-party library that should be dlopen()'d
|
|
instead.
|
|
"""
|
|
|
|
import argparse
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
|
|
GLIBC_RE: re.Pattern[str] = re.compile(r'\bGLIBC_(\d+)\.(\d+)(?:\.\d+)?\b')
|
|
NEEDED_RE: re.Pattern[str] = re.compile(r'\(NEEDED\)\s+Shared library:\s+\[([^\]]+)\]')
|
|
|
|
Version = tuple[int, int]
|
|
|
|
# Shared libraries that ship as part of the C library itself; always allowed.
|
|
# Matched by prefix so the soversion is not hardcoded. glibc splits libc/libm/etc.;
|
|
# musl bundles everything into a single libc whose soname encodes the architecture
|
|
# (e.g. libc.musl-x86_64.so.1, libc.musl-aarch64.so.1).
|
|
LIBC_LIB_PREFIXES: tuple[str, ...] = (
|
|
'libc.so.',
|
|
'libm.so.',
|
|
'libresolv.so.',
|
|
'libc.musl-',
|
|
)
|
|
|
|
# GCC runtime support libraries (stack unwinding, soft-float helpers, C++ standard library). The
|
|
# toolchain pulls these in automatically and they're part of every glibc toolchain. Matched by
|
|
# prefix so the soversion is not hardcoded.
|
|
GCC_LIB_PREFIXES: tuple[str, ...] = (
|
|
'libasan.so.',
|
|
'libclang_rt.asan.so',
|
|
'libclang_rt.ubsan.so',
|
|
'libubsan.so.',
|
|
'libgcc_s.so.',
|
|
'libstdc++.so.',
|
|
)
|
|
|
|
# Our own shared libraries; matched by prefix because the soname encodes the
|
|
# project version (libsystemd-shared-NNN.so, libsystemd-core-NNN.so).
|
|
INTERNAL_LIB_PREFIXES: tuple[str, ...] = (
|
|
'libsystemd-shared-',
|
|
'libsystemd-core-',
|
|
'libsystemd.so.',
|
|
'libudev.so.',
|
|
)
|
|
|
|
|
|
def parse_baseline(s: str) -> Version:
|
|
m = re.fullmatch(r'(\d+)\.(\d+)', s)
|
|
if not m:
|
|
raise argparse.ArgumentTypeError(f'baseline must be MAJOR.MINOR, got {s!r}')
|
|
return (int(m.group(1)), int(m.group(2)))
|
|
|
|
|
|
def is_elf(path: str) -> bool:
|
|
"""Cheap ELF magic check so readelf isn't run on non-ELF inputs."""
|
|
try:
|
|
with open(path, 'rb') as f:
|
|
return f.read(4) == b'\x7fELF'
|
|
except OSError:
|
|
return False
|
|
|
|
|
|
def is_internal(name: str) -> bool:
|
|
return any(name.startswith(p) for p in INTERNAL_LIB_PREFIXES)
|
|
|
|
|
|
def is_dynamic_linker(name: str) -> bool:
|
|
# The runtime linker's filename depends on the architecture: ld-linux-x86-64.so.2,
|
|
# ld-linux-aarch64.so.1, ld-linux-armhf.so.3, ld-linux-riscv64-lp64d.so.1 on the
|
|
# "ld-" naming; ld.so.1 (mips, ppc, s390 32-bit), ld64.so.1 (s390x, ppc64 BE),
|
|
# and ld64.so.2 (ppc64le) on the older naming. musl uses ld-musl-<arch>.so.1
|
|
# (already covered by the "ld-" prefix).
|
|
return name.startswith(('ld-', 'ld.so.', 'ld64.so.'))
|
|
|
|
|
|
def is_libc(name: str) -> bool:
|
|
return name.startswith(LIBC_LIB_PREFIXES)
|
|
|
|
|
|
def glibc_violations(path: str, baseline: Version) -> list[tuple[str, str]]:
|
|
"""Return a sorted list of (symbol, "GLIBC_X.Y") pairs above the baseline."""
|
|
out = subprocess.check_output(
|
|
['readelf', '-W', '--dyn-syms', path],
|
|
text=True,
|
|
stderr=subprocess.DEVNULL,
|
|
)
|
|
|
|
found: set[tuple[str, str]] = set()
|
|
for line in out.splitlines():
|
|
m = GLIBC_RE.search(line)
|
|
if not m:
|
|
continue
|
|
# readelf --dyn-syms lists both imported (Ndx=UND) and exported symbols;
|
|
# only the former carry a real link-time dependency on the listed glibc
|
|
# version. Skip anything that isn't UND.
|
|
parts = line.split()
|
|
if len(parts) < 8 or parts[6] != 'UND':
|
|
continue
|
|
ver: Version = (int(m.group(1)), int(m.group(2)))
|
|
if ver <= baseline:
|
|
continue
|
|
sym = next((t for t in parts if '@' in t), line.strip())
|
|
found.add((sym, m.group(0)))
|
|
return sorted(found)
|
|
|
|
|
|
def needed_violations(path: str) -> list[str]:
|
|
"""Return NEEDED entries outside the always-allowed set."""
|
|
out = subprocess.check_output(
|
|
['readelf', '-W', '-d', path],
|
|
text=True,
|
|
stderr=subprocess.DEVNULL,
|
|
)
|
|
|
|
bad: list[str] = []
|
|
for line in out.splitlines():
|
|
m = NEEDED_RE.search(line)
|
|
if not m:
|
|
continue
|
|
name = m.group(1)
|
|
if (
|
|
is_libc(name)
|
|
or name.startswith(GCC_LIB_PREFIXES)
|
|
or is_dynamic_linker(name)
|
|
or is_internal(name)
|
|
):
|
|
continue
|
|
bad.append(name)
|
|
return bad
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser(description=__doc__)
|
|
ap.add_argument(
|
|
'--baseline',
|
|
type=parse_baseline,
|
|
default=(2, 34),
|
|
help='Maximum allowed GLIBC version (default: 2.34)',
|
|
)
|
|
ap.add_argument('paths', nargs='*', metavar='PATH', help='ELF files to check')
|
|
args = ap.parse_args()
|
|
|
|
baseline: Version = args.baseline
|
|
checked = 0
|
|
failed = 0
|
|
for path in args.paths:
|
|
if not is_elf(path):
|
|
# Some inputs passed by meson may be scripts or non-ELF
|
|
# generators; silently skip those rather than fail.
|
|
continue
|
|
checked += 1
|
|
|
|
glibc_bad = glibc_violations(path, baseline)
|
|
needed_bad = needed_violations(path)
|
|
|
|
if glibc_bad or needed_bad:
|
|
failed += 1
|
|
print(f'{path}:', file=sys.stderr)
|
|
if glibc_bad:
|
|
print(f' imports symbols newer than GLIBC_{baseline[0]}.{baseline[1]}:', file=sys.stderr)
|
|
for sym, ver_str in glibc_bad:
|
|
print(f' {sym} ({ver_str})', file=sys.stderr)
|
|
if needed_bad:
|
|
print(' links against unexpected libraries (dlopen() them instead):', file=sys.stderr)
|
|
for name in needed_bad:
|
|
print(f' {name}', file=sys.stderr)
|
|
|
|
baseline_str = f'GLIBC_{baseline[0]}.{baseline[1]}'
|
|
if failed:
|
|
print(f'\nFAIL: {failed} of {checked} ELF objects failed the ABI checks.', file=sys.stderr)
|
|
return 1
|
|
print(
|
|
f'OK: {checked} ELF objects checked; all within {baseline_str} and with only allowed NEEDED entries.'
|
|
)
|
|
return 0
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|