Test that linux binary depends on glibc 2.18 and no other dynamic symbols (#372)

This regressed twice over the last two months (new floating point
function versions, and accidental dynamic linking against zlib).
We also want to avoid regressions when merging remote index.

The test is able to do a little bit more than we use in the automated
build (the --sym flag is unused, as is unversioned --lib=GLIBC) but they're
pretty useful when experimenting with how to fix things!

We run the test right at the end, because if it fails we want to be able
to download the binary artifact and inspect it.
Unfortunately by the nature of the test we can only run it when we
produce a build, so currently weekly.
This commit is contained in:
Sam McCall 2020-05-07 15:50:12 +02:00 committed by GitHub
parent 2de2ec4404
commit b51dc0092f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 94 additions and 1 deletions

View File

@ -142,6 +142,9 @@ jobs:
asset_name: clangd-${{ matrix.config.name }}-${{ github.event.release.tag_name }}.zip asset_name: clangd-${{ matrix.config.name }}-${{ github.event.release.tag_name }}.zip
asset_path: clangd.zip asset_path: clangd.zip
asset_content_type: application/zip asset_content_type: application/zip
- name: Check binary compatibility
if: matrix.config.name == 'linux'
run: .github/worflows/lib_compat_test.py --lib=GLIBC_2.18 "$CLANGD_DIR/bin/clangd"
# Create the release, and upload the artifacts to it. # Create the release, and upload the artifacts to it.
finalize: finalize:
runs-on: ubuntu-latest runs-on: ubuntu-latest

89
.github/workflows/lib_compat_test.py vendored Executable file
View File

@ -0,0 +1,89 @@
#!/usr/bin/env python3
# Verifies a binary uses only dynamic symbols from whitelisted library versions.
# Prints the disallowed symbols and their versions on failure.
# Usage: lib_compat_test.py bin/clangd --lib=GLIBC_2.18
import argparse
import subprocess
import sys
parser = argparse.ArgumentParser()
parser.add_argument("binary")
parser.add_argument(
"--lib",
action="append",
default=[],
help="Whitelist a library, e.g. GLIBC_2.18 or GLIBC",
)
parser.add_argument(
"--sym", action="append", default=[], help="Whitelist a symbol, e.g. crc32"
)
args = parser.parse_args()
# Parses GLIBC_2.3 into ("GLIBC", [2,3])
# Parses GLIBC into ("GLIBC", None)
def parse_version(version):
parts = version.rsplit("_", 1)
if len(parts) == 1:
return (version, None)
try:
return (parts[0], [int(p) for p in parts[1].split(".")])
except ValueError:
return (version, None)
lib_versions = dict([parse_version(v) for v in args.lib])
# Determines whether all symbols with version 'lib' are acceptable.
# A versioned library is name_x.y.z by convention.
def accept_lib(lib):
(lib, ver) = parse_version(lib)
if not lib in lib_versions: # Non-whitelisted library.
return False
if lib_versions[lib] is None: # Unrestricted version
return True
if ver is None: # Lib has non-numeric version, library restricts version.
return False
return ver <= lib_versions[lib]
# Determines whether an optionally-versioned symbol is acceptable.
# A versioned symbol is symbol@version as output by nm.
def accept_symbol(sym):
if sym in args.sym:
return True
split = sym.split("@", 1)
return (split[0] in args.sym) or (len(split) == 2 and accept_lib(split[1]))
# Run nm to find the undefined symbols, and check whether each is acceptable.
nm = subprocess.run(
["nm", "-uD", "--with-symbol-version", args.binary],
stdout=subprocess.PIPE,
text=True,
)
nm.check_returncode()
status = 0
for line in nm.stdout.splitlines():
# line = " U foo@GLIBC_2.3"
parts = line.split()
if len(parts) != 2:
print("Unparseable nm output: ", line, file=sys.stderr)
status = 2
continue
if parts[0] == "w": # Weak-undefined symbol, not actually required.
continue
if not accept_symbol(parts[1]):
print(parts[1])
status = 1
if status == 1:
print(
"Binary depends on disallowed symbols above. Use some combination of:\n"
" - relax the whitelist by adding --lib and --sym flags to this test\n"
" - force older symbol versions by updating lib_compat.h\n"
" - avoid dynamic dependencies by changing CMake configuration\n"
" - remove bad dependencies from the code",
file=sys.stderr,
)
sys.exit(status)

View File

@ -8,7 +8,8 @@ is being able to cut releases easily whenever we want.
The releases are just a zip archive containing the `clangd` binary, and the The releases are just a zip archive containing the `clangd` binary, and the
clang builtin headers. They should be runnable immediately after extracting the clang builtin headers. They should be runnable immediately after extracting the
archive. The linux binary has `libstdc++` and other dependencies statically archive. The linux binary has `libstdc++` and other dependencies statically
linked for maximum portability. linked for maximum portability, and requires glibc 2.18 (the first version with
`thread_local` support).
## Creating a release manually ## Creating a release manually