From d4b1472fe085df8419c98714ecc3d3617ad8b8e3 Mon Sep 17 00:00:00 2001 From: Tyler Stapler Date: Tue, 12 Jul 2022 12:35:54 -0700 Subject: [PATCH] Add a test to images/dnsmasq/validate.py to verify dnsmasq dynamic deps --- images/dnsmasq/validate.sh | 6 + images/dnsmasq/validate_dynamic_deps.py | 151 ++++++++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 images/dnsmasq/validate_dynamic_deps.py diff --git a/images/dnsmasq/validate.sh b/images/dnsmasq/validate.sh index e43bc53e8..f8f84c601 100755 --- a/images/dnsmasq/validate.sh +++ b/images/dnsmasq/validate.sh @@ -1,4 +1,5 @@ #!/bin/bash +set -e if [ -z "$IMAGE" ]; then echo "IMAGE needs to be set to the dnsmasq image" @@ -6,5 +7,10 @@ fi echo "Checking ${IMAGE} ${ARCH}" +# Check that dnsmasq is correctly compiled and has the right +# dynamic libraries to run. +./validate_dynamic_deps.py --image ${IMAGE} --target-bin /usr/sbin/dnsmasq + # Check that dnsmasq is able to start. exec docker run --rm -- "${IMAGE}" --version >/dev/null + diff --git a/images/dnsmasq/validate_dynamic_deps.py b/images/dnsmasq/validate_dynamic_deps.py new file mode 100644 index 000000000..db86aa557 --- /dev/null +++ b/images/dnsmasq/validate_dynamic_deps.py @@ -0,0 +1,151 @@ +# /usr/bin/env python3 +# This script requires python 3.8+ +import os +import shutil +import subprocess +import pathlib +import sys + +IMAGE_DIR = '.test-images' + + +def replace_img_special_chars(image_ref: str): + return image_ref.lower().replace('/', '_').replace(':', '_') + + +def relative_to_root(path: pathlib.Path) -> pathlib.Path: + return path.relative_to(path.anchor) + + +def detect_architecture(cwd: pathlib.Path, target_binary: pathlib.Path) -> str: + result = subprocess.run(['file', target_binary], + capture_output=True, + text=True, + cwd=cwd) + return result.stdout.split(",")[1].strip() + + +def untar_container_image(image_ref: str, output_dir: pathlib.Path): + container_name = replace_img_special_chars(image_ref) + subprocess.run(['docker', 'create', '--name', container_name, image_ref], + capture_output=True) + subprocess.run(f'docker export {container_name} | tar x', + shell=True, + cwd=output_dir, + capture_output=True) + subprocess.run(['docker', 'rm', container_name], capture_output=True) + + +def detect_dependencies(cwd: pathlib.Path, target_binary: pathlib.Path): + result = subprocess.run(['objdump', '-p', target_binary], + capture_output=True, + text=True, + cwd=cwd) + deps = [] + for line in result.stdout.splitlines(): + if "NEEDED" in line: + deps.append(line.split()[1]) + return deps + + +def find_dependency_path(root_dir: pathlib.Path, dependency_name: str): + paths = sorted(root_dir.glob(f'**/{dependency_name}')) + return paths[0] if paths else None + + +def resolve_container_link(root_dir: pathlib.Path, link: pathlib.Path): + """Resolve the provided link, make absolute paths relative to root_dir""" + resolved = os.readlink(link) + + if str(resolved).startswith('/'): + return root_dir.joinpath(relative_to_root(resolved)) + return link.resolve().relative_to(root_dir.resolve()) + + +def main(image_ref: str, target_binary: str, clean=True): + print(f"Analyzing the binary {target_binary} from {image_ref}") + container_name = replace_img_special_chars(image_ref) + output_dir = pathlib.Path(IMAGE_DIR, container_name) + target_binary = relative_to_root(pathlib.Path(target_binary)) + + output_dir.mkdir(parents=True, exist_ok=True) + untar_container_image(image_ref, output_dir) + target_binary_arch = detect_architecture(output_dir, target_binary) + deps = detect_dependencies(output_dir, target_binary) + + seen = set() + missing = set() + wrong_arch = {} + while deps: + current = deps.pop() + + if current in seen: + continue + + current_path = find_dependency_path(output_dir, current) + + if not current_path: + missing.add(current) + continue + + if current_path.is_symlink(): + current_path = resolve_container_link(output_dir, current_path) + + current_arch = detect_architecture(output_dir, current_path) + if current_arch != target_binary_arch: + wrong_arch[current_path] = current_arch + + current_dependencies = detect_dependencies(output_dir, current_path) + + deps.extend(current_dependencies) + + seen.add(current) + + print("Dependencies:") + for dep in seen: + print(dep) + print() + + exit_code = 0 + if wrong_arch: + exit_code = 1 + print("Wrong Architecture:") + for dep, arch in wrong_arch.items(): + print(f"{dep} - {arch}") + print() + + if missing: + exit_code = 1 + print("Missing Dependencies:") + for miss in missing: + print(miss) + print() + + if exit_code == 0: + print(f"All dependencies for {target_binary} are present") + + if clean: + shutil.rmtree(output_dir) + + sys.exit(exit_code) + + +if __name__ == '__main__': + import argparse + + parser = argparse.ArgumentParser( + description= + "A script to validate check if all of a binary's dynamically linked dependencies are present in the container" + ) + parser.add_argument("--image", + help="A docker image to download and analyze", + required=True) + parser.add_argument("--target-bin", + help="The binary to analyze within the IMAGE", + required=True) + parser.add_argument("--skip-clean", + action="store_false", + help="Do not clean up the intermediate artifacts") + + args = parser.parse_args() + main(args.image, args.target_bin, clean=args.skip_clean)