From f9578f58101c9a1e9d6860b1a8972667f8094c04 Mon Sep 17 00:00:00 2001 From: Maciej Borzecki Date: Fri, 10 Jan 2025 09:12:00 +0100 Subject: [PATCH] build-aux/snap/local: cleanup patch and verification helper scripts (#14901) * build-aux/snap/local: cleanup patch and verification helper scripts Improve the patching and verification scripts used during snap build. Signed-off-by: Maciej Borzecki * snapcraft: update call sites of ELF patching/verification scripts Signed-off-by: Maciej Borzecki --------- Signed-off-by: Maciej Borzecki --- build-aux/snap/local/patch-dl.py | 128 ++++++++++++++++++------------ build-aux/snap/local/verify-dl.py | 72 +++++++++++++---- build-aux/snap/snapcraft.yaml | 6 +- 3 files changed, 136 insertions(+), 70 deletions(-) diff --git a/build-aux/snap/local/patch-dl.py b/build-aux/snap/local/patch-dl.py index 117a39f8c94..a22842df5d3 100755 --- a/build-aux/snap/local/patch-dl.py +++ b/build-aux/snap/local/patch-dl.py @@ -7,72 +7,98 @@ import os import shutil import subprocess -import sys import tempfile +import logging from elftools.elf.elffile import ELFFile -parser = argparse.ArgumentParser() +def parse_arguments() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="patch ELF binaries to request a specific interpreter", + ) + craft_prime = os.environ.get("CRAFT_PRIME") + parser.add_argument( + "--prime", + nargs=1, + default=craft_prime, + required=(craft_prime is None), + help="snapcraft part priming location", + ) -craft_prime = os.environ.get('CRAFT_PRIME') -parser.add_argument('--prime', nargs=1, default=craft_prime, required=(craft_prime is None)) + craft_part_install = os.environ.get("CRAFT_PART_INSTALL") + parser.add_argument( + "--install", + nargs=1, + default=craft_part_install, + required=(craft_part_install is None), + help="snapcraft part install location", + ) -craft_part_install = os.environ.get('CRAFT_PART_INSTALL') -parser.add_argument('--install', nargs=1, default=craft_part_install, required=(craft_part_install is None)) + parser.add_argument("interp", help="ELF interpreter to use") -parser.add_argument('linker') + return parser.parse_args() -args = parser.parse_args() -def is_shared_exec(path): - with open(path, 'rb') as f: - if f.read(4) != b'\x7fELF': +def is_shared_exec(path: str) -> bool: + with open(path, "rb") as f: + if f.read(4) != b"\x7fELF": return False f.seek(0, 0) elf = ELFFile(f) for segment in elf.iter_segments(): # TODO: use iter_segments(type='PT_INTERP') - if segment['p_type'] == 'PT_INTERP': + if segment["p_type"] == "PT_INTERP": return True return False -owned_executables = [] - -for dirpath, dirnames, filenames in os.walk(args.install): - for filename in filenames: - path = os.path.join(dirpath, filename) - if os.path.islink(path): - continue - if not is_shared_exec(path): - continue - rel = os.path.relpath(path, args.install) - # Now we need to know if the file in $CRAFT_PRIME is actually - # owned by the current part and see if it is hard-linked to a - # corresponding file in $CRAFT_PART_INSTALL. - # - # Even if we break the hard-links before, subsequent builds will - # re-introduce the hard-links in `crafctl default` call in the - # `override-prime`. - prime_path = os.path.join(args.prime, rel) - install_st = os.lstat(path) - prime_st = os.lstat(path) - if install_st.st_dev != prime_st.st_dev: - continue - if install_st.st_ino != prime_st.st_ino: - continue - owned_executables.append(prime_path) - -for path in owned_executables: - # Because files in $CRAFT_PRIME, $CRAFT_STAGE, and $CRAFT_PART_INSTALL are hard-linked, - # we need to copy the file first to avoid writing back to the content of other directories. - with tempfile.NamedTemporaryFile(dir=os.path.dirname(path), - prefix='{}-'.format(os.path.basename(path))) as f: - with open(path, 'rb') as orig: - shutil.copyfileobj(orig, f) - f.flush() - print(f'Running patchelf for "{f.name}"', file=sys.stderr) - subprocess.run(['patchelf', '--set-interpreter', args.linker, f.name], check=True) - shutil.copystat(path, f.name) - os.unlink(path) - os.link(f.name, path) + +def main(args) -> None: + owned_executables = [] + + for dirpath, _, filenames in os.walk(args.install): + for filename in filenames: + path = os.path.join(dirpath, filename) + if os.path.islink(path): + continue + if not is_shared_exec(path): + continue + logging.debug("found owned ELF binary: %s", path) + rel = os.path.relpath(path, args.install) + # Now we need to know if the file in $CRAFT_PRIME is actually + # owned by the current part and see if it is hard-linked to a + # corresponding file in $CRAFT_PART_INSTALL. + # + # Even if we break the hard-links before, subsequent builds will + # re-introduce the hard-links in `crafctl default` call in the + # `override-prime`. + prime_path = os.path.join(args.prime, rel) + install_st = os.lstat(path) + prime_st = os.lstat(path) + if install_st.st_dev != prime_st.st_dev: + continue + if install_st.st_ino != prime_st.st_ino: + continue + owned_executables.append(prime_path) + + for path in owned_executables: + # Because files in $CRAFT_PRIME, $CRAFT_STAGE, and $CRAFT_PART_INSTALL are hard-linked, + # we need to copy the file first to avoid writing back to the content of other directories. + with tempfile.NamedTemporaryFile( + dir=os.path.dirname(path), prefix=f"{os.path.basename(path)}-" + ) as f: + with open(path, "rb") as orig: + shutil.copyfileobj(orig, f) + f.flush() + logging.info("patching ELF binary %s", path) + subprocess.run( + ["patchelf", "--set-interpreter", args.interp, f.name], check=True + ) + shutil.copystat(path, f.name) + os.unlink(path) + os.link(f.name, path) + + +if __name__ == "__main__": + logging.basicConfig(level=logging.DEBUG) + main(parse_arguments()) diff --git a/build-aux/snap/local/verify-dl.py b/build-aux/snap/local/verify-dl.py index e982e37222b..d77de35eafe 100755 --- a/build-aux/snap/local/verify-dl.py +++ b/build-aux/snap/local/verify-dl.py @@ -5,25 +5,63 @@ import os import sys +import logging +import argparse from elftools.elf.elffile import ELFFile -errors = 0 -for dirpath, dirnames, filenames in os.walk(sys.argv[1]): - for filename in filenames: - path = os.path.join(dirpath, filename) - if os.path.islink(path): - continue - with open(path, 'rb') as f: - if f.read(4) != b'\x7fELF': +def parse_arguments() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="verify requested interpreter of ELF binaries" + ) + craft_prime = os.environ.get("CRAFT_PRIME") + parser.add_argument( + "--prime", + default=craft_prime, + required=(craft_prime is None), + help="snapcraft part priming location (or rootdir of a snap)", + ) + parser.add_argument("interp", help="Expected interpreter") + + return parser.parse_args() + + +def main(opts) -> None: + errors: list[str] = [] + + for dirpath, _, filenames in os.walk(opts.prime): + for filename in filenames: + path = os.path.join(dirpath, filename) + if os.path.islink(path): continue - f.seek(0, 0) - elf = ELFFile(f) - for segment in elf.iter_segments(): - # TODO: use iter_segments(type='PT_INTERP') - if segment['p_type'] == 'PT_INTERP' and segment.get_interp_name() != sys.argv[2]: - print('{}: Expected interpreter to be "{}", got "{}"'.format(path, sys.argv[2], segment.get_interp_name()), file=sys.stderr) - errors +=1 - -sys.exit(errors) + with open(path, "rb") as f: + if f.read(4) != b"\x7fELF": + continue + f.seek(0, 0) + + logging.debug("checking ELF binary: %s", path) + + elf = ELFFile(f) + for segment in elf.iter_segments(): + # TODO: use iter_segments(type='PT_INTERP') + if ( + segment["p_type"] == "PT_INTERP" + and segment.get_interp_name() != opts.interp + ): + logging.error( + '%s: expected interpreter to be "%s", got "%s"', + path, + sys.argv[2], + segment.get_interp_name(), + ) + errors.append(path) + + if errors: + badlist = "\n".join(["- " + n for n in errors]) + raise RuntimeError(f"found binaries with incorrect ELF interpreter:\n{badlist}") + + +if __name__ == "__main__": + logging.basicConfig(level=logging.DEBUG) + main(parse_arguments()) diff --git a/build-aux/snap/snapcraft.yaml b/build-aux/snap/snapcraft.yaml index 6c3a0ee6278..673a79e7c63 100644 --- a/build-aux/snap/snapcraft.yaml +++ b/build-aux/snap/snapcraft.yaml @@ -441,7 +441,8 @@ parts: - -fips-build-lp override-prime: | craftctl default - python3 "${CRAFT_PROJECT_DIR}/build-aux/snap/local/patch-dl.py" "/snap/snapd/current/usr/lib/${CRAFT_ARCH_TRIPLET_BUILD_FOR}/${DYNAMIC_LINKER}" + "${CRAFT_PROJECT_DIR}/build-aux/snap/local/patch-dl.py" \ + "/snap/snapd/current/usr/lib/${CRAFT_ARCH_TRIPLET_BUILD_FOR}/${DYNAMIC_LINKER}" libcrypto-fips: plugin: nil @@ -524,4 +525,5 @@ parts: <<: - *dynamic-linker override-prime: | - python3 "${CRAFT_PROJECT_DIR}/build-aux/snap/local/verify-dl.py" "${CRAFT_PRIME}" "/snap/snapd/current/usr/lib/${CRAFT_ARCH_TRIPLET_BUILD_FOR}/${DYNAMIC_LINKER}" ";" + "${CRAFT_PROJECT_DIR}/build-aux/snap/local/verify-dl.py" \ + "/snap/snapd/current/usr/lib/${CRAFT_ARCH_TRIPLET_BUILD_FOR}/${DYNAMIC_LINKER}"