#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # Copyright 2019 The ChromiumOS Authors # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """Diff 2 chromiumos images by comparing each elf file. The script diffs every *ELF* files by dissembling every *executable* section, which means it is not a FULL elf differ. A simple usage example - chromiumos_image_diff.py --image1 image-path-1 --image2 image-path-2 Note that image path should be inside the chroot, if not (ie, image is downloaded from web), please specify a chromiumos checkout via "--chromeos_root". And this script should be executed outside chroot. """ __author__ = "shenhan@google.com (Han Shen)" import argparse import os import re import sys import tempfile from cros_utils import command_executer from cros_utils import logger from cros_utils import misc import image_chromeos class CrosImage(object): """A cros image object.""" def __init__(self, image, chromeos_root, no_unmount): self.image = image self.chromeos_root = chromeos_root self.mounted = False self._ce = command_executer.GetCommandExecuter() self.logger = logger.GetLogger() self.elf_files = [] self.no_unmount = no_unmount self.unmount_script = "" self.stateful = "" self.rootfs = "" def MountImage(self, mount_basename): """Mount/unpack the image.""" if mount_basename: self.rootfs = "/tmp/{0}.rootfs".format(mount_basename) self.stateful = "/tmp/{0}.stateful".format(mount_basename) self.unmount_script = "/tmp/{0}.unmount.sh".format(mount_basename) else: self.rootfs = tempfile.mkdtemp( suffix=".rootfs", prefix="chromiumos_image_diff" ) ## rootfs is like /tmp/tmpxyz012.rootfs. match = re.match(r"^(.*)\.rootfs$", self.rootfs) basename = match.group(1) self.stateful = basename + ".stateful" os.mkdir(self.stateful) self.unmount_script = "{0}.unmount.sh".format(basename) self.logger.LogOutput( 'Mounting "{0}" onto "{1}" and "{2}"'.format( self.image, self.rootfs, self.stateful ) ) ## First of all creating an unmount image self.CreateUnmountScript() command = image_chromeos.GetImageMountCommand( self.image, self.rootfs, self.stateful ) rv = self._ce.RunCommand(command, print_to_console=True) self.mounted = rv == 0 if not self.mounted: self.logger.LogError( 'Failed to mount "{0}" onto "{1}" and "{2}".'.format( self.image, self.rootfs, self.stateful ) ) return self.mounted def CreateUnmountScript(self): command = ( "sudo umount {r}/usr/local {r}/usr/share/oem " "{r}/var {r}/mnt/stateful_partition {r}; sudo umount {s} ; " "rmdir {r} ; rmdir {s}\n" ).format(r=self.rootfs, s=self.stateful) f = open(self.unmount_script, "w", encoding="utf-8") f.write(command) f.close() self._ce.RunCommand( "chmod +x {}".format(self.unmount_script), print_to_console=False ) self.logger.LogOutput( 'Created an unmount script - "{0}"'.format(self.unmount_script) ) def UnmountImage(self): """Unmount the image and delete mount point.""" self.logger.LogOutput( 'Unmounting image "{0}" from "{1}" and "{2}"'.format( self.image, self.rootfs, self.stateful ) ) if self.mounted: command = 'bash "{0}"'.format(self.unmount_script) if self.no_unmount: self.logger.LogOutput( ( "Please unmount manually - \n" '\t bash "{0}"'.format(self.unmount_script) ) ) else: if self._ce.RunCommand(command, print_to_console=True) == 0: self._ce.RunCommand("rm {0}".format(self.unmount_script)) self.mounted = False self.rootfs = None self.stateful = None self.unmount_script = None return not self.mounted def FindElfFiles(self): """Find all elf files for the image. Returns: Always true """ self.logger.LogOutput( 'Finding all elf files in "{0}" ...'.format(self.rootfs) ) # Note '\;' must be prefixed by 'r'. command = ( 'find "{0}" -type f -exec ' 'bash -c \'file -b "{{}}" | grep -q "ELF"\'' r" \; " r'-exec echo "{{}}" \;' ).format(self.rootfs) self.logger.LogCmd(command) _, out, _ = self._ce.RunCommandWOutput(command, print_to_console=False) self.elf_files = out.splitlines() self.logger.LogOutput( "Total {0} elf files found.".format(len(self.elf_files)) ) return True class ImageComparator(object): """A class that wraps comparsion actions.""" def __init__(self, images, diff_file): self.images = images self.logger = logger.GetLogger() self.diff_file = diff_file self.tempf1 = None self.tempf2 = None def Cleanup(self): if self.tempf1 and self.tempf2: command_executer.GetCommandExecuter().RunCommand( "rm {0} {1}".format(self.tempf1, self.tempf2) ) logger.GetLogger( 'Removed "{0}" and "{1}".'.format(self.tempf1, self.tempf2) ) def CheckElfFileSetEquality(self): """Checking whether images have exactly number of elf files.""" self.logger.LogOutput("Checking elf file equality ...") i1 = self.images[0] i2 = self.images[1] t1 = i1.rootfs + "/" elfset1 = {e.replace(t1, "") for e in i1.elf_files} t2 = i2.rootfs + "/" elfset2 = {e.replace(t2, "") for e in i2.elf_files} dif1 = elfset1.difference(elfset2) msg = None if dif1: msg = 'The following files are not in "{image}" - "{rootfs}":\n'.format( image=i2.image, rootfs=i2.rootfs ) for d in dif1: msg += "\t" + d + "\n" dif2 = elfset2.difference(elfset1) if dif2: msg = 'The following files are not in "{image}" - "{rootfs}":\n'.format( image=i1.image, rootfs=i1.rootfs ) for d in dif2: msg += "\t" + d + "\n" if msg: self.logger.LogError(msg) return False return True def CompareImages(self): """Do the comparsion work.""" if not self.CheckElfFileSetEquality(): return False mismatch_list = [] match_count = 0 i1 = self.images[0] i2 = self.images[1] self.logger.LogOutput( "Start comparing {0} elf file by file ...".format(len(i1.elf_files)) ) ## Note - i1.elf_files and i2.elf_files have exactly the same entries here. ## Create 2 temp files to be used for all disassembed files. handle, self.tempf1 = tempfile.mkstemp() os.close(handle) # We do not need the handle handle, self.tempf2 = tempfile.mkstemp() os.close(handle) cmde = command_executer.GetCommandExecuter() for elf1 in i1.elf_files: tmp_rootfs = i1.rootfs + "/" f1 = elf1.replace(tmp_rootfs, "") full_path1 = elf1 full_path2 = elf1.replace(i1.rootfs, i2.rootfs) if full_path1 == full_path2: self.logger.LogError( "Error: We're comparing the SAME file - {0}".format(f1) ) continue command = ( 'objdump -d "{f1}" > {tempf1} ; ' 'objdump -d "{f2}" > {tempf2} ; ' # Remove path string inside the dissemble "sed -i 's!{rootfs1}!!g' {tempf1} ; " "sed -i 's!{rootfs2}!!g' {tempf2} ; " "diff {tempf1} {tempf2} 1>/dev/null 2>&1" ).format( f1=full_path1, f2=full_path2, rootfs1=i1.rootfs, rootfs2=i2.rootfs, tempf1=self.tempf1, tempf2=self.tempf2, ) ret = cmde.RunCommand(command, print_to_console=False) if ret != 0: self.logger.LogOutput( '*** Not match - "{0}" "{1}"'.format(full_path1, full_path2) ) mismatch_list.append(f1) if self.diff_file: command = ( 'echo "Diffs of disassemble of "{f1}" and "{f2}"" ' ">> {diff_file} ; diff {tempf1} {tempf2} " ">> {diff_file}" ).format( f1=full_path1, f2=full_path2, diff_file=self.diff_file, tempf1=self.tempf1, tempf2=self.tempf2, ) cmde.RunCommand(command, print_to_console=False) else: match_count += 1 ## End of comparing every elf files. if not mismatch_list: self.logger.LogOutput( "** COOL, ALL {0} BINARIES MATCHED!! **".format(match_count) ) return True mismatch_str = "Found {0} mismatch:\n".format(len(mismatch_list)) for b in mismatch_list: mismatch_str += "\t" + b + "\n" self.logger.LogOutput(mismatch_str) return False def Main(argv): """The main function.""" command_executer.InitCommandExecuter() images = [] parser = argparse.ArgumentParser() parser.add_argument( "--no_unmount", action="store_true", dest="no_unmount", default=False, help="Do not unmount after finish, this is useful for debugging.", ) parser.add_argument( "--chromeos_root", dest="chromeos_root", default=None, action="store", help=( "[Optional] Specify a chromeos tree instead of " "deducing it from image path so that we can compare " "2 images that are downloaded." ), ) parser.add_argument( "--mount_basename", dest="mount_basename", default=None, action="store", help=( "Specify a meaningful name for the mount point. With this being " 'set, the mount points would be "/tmp/mount_basename.x.rootfs" ' ' and "/tmp/mount_basename.x.stateful". (x is 1 or 2).' ), ) parser.add_argument( "--diff_file", dest="diff_file", default=None, help="Dumping all the diffs (if any) to the diff file", ) parser.add_argument( "--image1", dest="image1", default=None, required=True, help=("Image 1 file name."), ) parser.add_argument( "--image2", dest="image2", default=None, required=True, help=("Image 2 file name."), ) options = parser.parse_args(argv[1:]) if options.mount_basename and options.mount_basename.find("/") >= 0: logger.GetLogger().LogError( '"--mount_basename" must be a name, not a path.' ) parser.print_help() return 1 result = False image_comparator = None try: for i, image_path in enumerate( [options.image1, options.image2], start=1 ): image_path = os.path.realpath(image_path) if not os.path.isfile(image_path): logger.GetLogger().LogError( '"{0}" is not a file.'.format(image_path) ) return 1 chromeos_root = None if options.chromeos_root: chromeos_root = options.chromeos_root else: ## Deduce chromeos root from image t = image_path while t != "/": if misc.IsChromeOsTree(t): break t = os.path.dirname(t) if misc.IsChromeOsTree(t): chromeos_root = t if not chromeos_root: logger.GetLogger().LogError( "Please provide a valid chromeos root via --chromeos_root" ) return 1 image = CrosImage(image_path, chromeos_root, options.no_unmount) if options.mount_basename: mount_basename = "{basename}.{index}".format( basename=options.mount_basename, index=i ) else: mount_basename = None if image.MountImage(mount_basename): images.append(image) image.FindElfFiles() if len(images) == 2: image_comparator = ImageComparator(images, options.diff_file) result = image_comparator.CompareImages() finally: for image in images: image.UnmountImage() if image_comparator: image_comparator.Cleanup() return 0 if result else 1 if __name__ == "__main__": Main(sys.argv)