1*760c253cSXin Li#!/usr/bin/env python3 2*760c253cSXin Li# -*- coding: utf-8 -*- 3*760c253cSXin Li# 4*760c253cSXin Li# Copyright 2019 The ChromiumOS Authors 5*760c253cSXin Li# Use of this source code is governed by a BSD-style license that can be 6*760c253cSXin Li# found in the LICENSE file. 7*760c253cSXin Li 8*760c253cSXin Li"""Diff 2 chromiumos images by comparing each elf file. 9*760c253cSXin Li 10*760c253cSXin Li The script diffs every *ELF* files by dissembling every *executable* 11*760c253cSXin Li section, which means it is not a FULL elf differ. 12*760c253cSXin Li 13*760c253cSXin Li A simple usage example - 14*760c253cSXin Li chromiumos_image_diff.py --image1 image-path-1 --image2 image-path-2 15*760c253cSXin Li 16*760c253cSXin Li Note that image path should be inside the chroot, if not (ie, image is 17*760c253cSXin Li downloaded from web), please specify a chromiumos checkout via 18*760c253cSXin Li "--chromeos_root". 19*760c253cSXin Li 20*760c253cSXin Li And this script should be executed outside chroot. 21*760c253cSXin Li""" 22*760c253cSXin Li 23*760c253cSXin Li 24*760c253cSXin Li__author__ = "[email protected] (Han Shen)" 25*760c253cSXin Li 26*760c253cSXin Liimport argparse 27*760c253cSXin Liimport os 28*760c253cSXin Liimport re 29*760c253cSXin Liimport sys 30*760c253cSXin Liimport tempfile 31*760c253cSXin Li 32*760c253cSXin Lifrom cros_utils import command_executer 33*760c253cSXin Lifrom cros_utils import logger 34*760c253cSXin Lifrom cros_utils import misc 35*760c253cSXin Liimport image_chromeos 36*760c253cSXin Li 37*760c253cSXin Li 38*760c253cSXin Liclass CrosImage(object): 39*760c253cSXin Li """A cros image object.""" 40*760c253cSXin Li 41*760c253cSXin Li def __init__(self, image, chromeos_root, no_unmount): 42*760c253cSXin Li self.image = image 43*760c253cSXin Li self.chromeos_root = chromeos_root 44*760c253cSXin Li self.mounted = False 45*760c253cSXin Li self._ce = command_executer.GetCommandExecuter() 46*760c253cSXin Li self.logger = logger.GetLogger() 47*760c253cSXin Li self.elf_files = [] 48*760c253cSXin Li self.no_unmount = no_unmount 49*760c253cSXin Li self.unmount_script = "" 50*760c253cSXin Li self.stateful = "" 51*760c253cSXin Li self.rootfs = "" 52*760c253cSXin Li 53*760c253cSXin Li def MountImage(self, mount_basename): 54*760c253cSXin Li """Mount/unpack the image.""" 55*760c253cSXin Li 56*760c253cSXin Li if mount_basename: 57*760c253cSXin Li self.rootfs = "/tmp/{0}.rootfs".format(mount_basename) 58*760c253cSXin Li self.stateful = "/tmp/{0}.stateful".format(mount_basename) 59*760c253cSXin Li self.unmount_script = "/tmp/{0}.unmount.sh".format(mount_basename) 60*760c253cSXin Li else: 61*760c253cSXin Li self.rootfs = tempfile.mkdtemp( 62*760c253cSXin Li suffix=".rootfs", prefix="chromiumos_image_diff" 63*760c253cSXin Li ) 64*760c253cSXin Li ## rootfs is like /tmp/tmpxyz012.rootfs. 65*760c253cSXin Li match = re.match(r"^(.*)\.rootfs$", self.rootfs) 66*760c253cSXin Li basename = match.group(1) 67*760c253cSXin Li self.stateful = basename + ".stateful" 68*760c253cSXin Li os.mkdir(self.stateful) 69*760c253cSXin Li self.unmount_script = "{0}.unmount.sh".format(basename) 70*760c253cSXin Li 71*760c253cSXin Li self.logger.LogOutput( 72*760c253cSXin Li 'Mounting "{0}" onto "{1}" and "{2}"'.format( 73*760c253cSXin Li self.image, self.rootfs, self.stateful 74*760c253cSXin Li ) 75*760c253cSXin Li ) 76*760c253cSXin Li ## First of all creating an unmount image 77*760c253cSXin Li self.CreateUnmountScript() 78*760c253cSXin Li command = image_chromeos.GetImageMountCommand( 79*760c253cSXin Li self.image, self.rootfs, self.stateful 80*760c253cSXin Li ) 81*760c253cSXin Li rv = self._ce.RunCommand(command, print_to_console=True) 82*760c253cSXin Li self.mounted = rv == 0 83*760c253cSXin Li if not self.mounted: 84*760c253cSXin Li self.logger.LogError( 85*760c253cSXin Li 'Failed to mount "{0}" onto "{1}" and "{2}".'.format( 86*760c253cSXin Li self.image, self.rootfs, self.stateful 87*760c253cSXin Li ) 88*760c253cSXin Li ) 89*760c253cSXin Li return self.mounted 90*760c253cSXin Li 91*760c253cSXin Li def CreateUnmountScript(self): 92*760c253cSXin Li command = ( 93*760c253cSXin Li "sudo umount {r}/usr/local {r}/usr/share/oem " 94*760c253cSXin Li "{r}/var {r}/mnt/stateful_partition {r}; sudo umount {s} ; " 95*760c253cSXin Li "rmdir {r} ; rmdir {s}\n" 96*760c253cSXin Li ).format(r=self.rootfs, s=self.stateful) 97*760c253cSXin Li f = open(self.unmount_script, "w", encoding="utf-8") 98*760c253cSXin Li f.write(command) 99*760c253cSXin Li f.close() 100*760c253cSXin Li self._ce.RunCommand( 101*760c253cSXin Li "chmod +x {}".format(self.unmount_script), print_to_console=False 102*760c253cSXin Li ) 103*760c253cSXin Li self.logger.LogOutput( 104*760c253cSXin Li 'Created an unmount script - "{0}"'.format(self.unmount_script) 105*760c253cSXin Li ) 106*760c253cSXin Li 107*760c253cSXin Li def UnmountImage(self): 108*760c253cSXin Li """Unmount the image and delete mount point.""" 109*760c253cSXin Li 110*760c253cSXin Li self.logger.LogOutput( 111*760c253cSXin Li 'Unmounting image "{0}" from "{1}" and "{2}"'.format( 112*760c253cSXin Li self.image, self.rootfs, self.stateful 113*760c253cSXin Li ) 114*760c253cSXin Li ) 115*760c253cSXin Li if self.mounted: 116*760c253cSXin Li command = 'bash "{0}"'.format(self.unmount_script) 117*760c253cSXin Li if self.no_unmount: 118*760c253cSXin Li self.logger.LogOutput( 119*760c253cSXin Li ( 120*760c253cSXin Li "Please unmount manually - \n" 121*760c253cSXin Li '\t bash "{0}"'.format(self.unmount_script) 122*760c253cSXin Li ) 123*760c253cSXin Li ) 124*760c253cSXin Li else: 125*760c253cSXin Li if self._ce.RunCommand(command, print_to_console=True) == 0: 126*760c253cSXin Li self._ce.RunCommand("rm {0}".format(self.unmount_script)) 127*760c253cSXin Li self.mounted = False 128*760c253cSXin Li self.rootfs = None 129*760c253cSXin Li self.stateful = None 130*760c253cSXin Li self.unmount_script = None 131*760c253cSXin Li 132*760c253cSXin Li return not self.mounted 133*760c253cSXin Li 134*760c253cSXin Li def FindElfFiles(self): 135*760c253cSXin Li """Find all elf files for the image. 136*760c253cSXin Li 137*760c253cSXin Li Returns: 138*760c253cSXin Li Always true 139*760c253cSXin Li """ 140*760c253cSXin Li 141*760c253cSXin Li self.logger.LogOutput( 142*760c253cSXin Li 'Finding all elf files in "{0}" ...'.format(self.rootfs) 143*760c253cSXin Li ) 144*760c253cSXin Li # Note '\;' must be prefixed by 'r'. 145*760c253cSXin Li command = ( 146*760c253cSXin Li 'find "{0}" -type f -exec ' 147*760c253cSXin Li 'bash -c \'file -b "{{}}" | grep -q "ELF"\'' 148*760c253cSXin Li r" \; " 149*760c253cSXin Li r'-exec echo "{{}}" \;' 150*760c253cSXin Li ).format(self.rootfs) 151*760c253cSXin Li self.logger.LogCmd(command) 152*760c253cSXin Li _, out, _ = self._ce.RunCommandWOutput(command, print_to_console=False) 153*760c253cSXin Li self.elf_files = out.splitlines() 154*760c253cSXin Li self.logger.LogOutput( 155*760c253cSXin Li "Total {0} elf files found.".format(len(self.elf_files)) 156*760c253cSXin Li ) 157*760c253cSXin Li return True 158*760c253cSXin Li 159*760c253cSXin Li 160*760c253cSXin Liclass ImageComparator(object): 161*760c253cSXin Li """A class that wraps comparsion actions.""" 162*760c253cSXin Li 163*760c253cSXin Li def __init__(self, images, diff_file): 164*760c253cSXin Li self.images = images 165*760c253cSXin Li self.logger = logger.GetLogger() 166*760c253cSXin Li self.diff_file = diff_file 167*760c253cSXin Li self.tempf1 = None 168*760c253cSXin Li self.tempf2 = None 169*760c253cSXin Li 170*760c253cSXin Li def Cleanup(self): 171*760c253cSXin Li if self.tempf1 and self.tempf2: 172*760c253cSXin Li command_executer.GetCommandExecuter().RunCommand( 173*760c253cSXin Li "rm {0} {1}".format(self.tempf1, self.tempf2) 174*760c253cSXin Li ) 175*760c253cSXin Li logger.GetLogger( 176*760c253cSXin Li 'Removed "{0}" and "{1}".'.format(self.tempf1, self.tempf2) 177*760c253cSXin Li ) 178*760c253cSXin Li 179*760c253cSXin Li def CheckElfFileSetEquality(self): 180*760c253cSXin Li """Checking whether images have exactly number of elf files.""" 181*760c253cSXin Li 182*760c253cSXin Li self.logger.LogOutput("Checking elf file equality ...") 183*760c253cSXin Li i1 = self.images[0] 184*760c253cSXin Li i2 = self.images[1] 185*760c253cSXin Li t1 = i1.rootfs + "/" 186*760c253cSXin Li elfset1 = {e.replace(t1, "") for e in i1.elf_files} 187*760c253cSXin Li t2 = i2.rootfs + "/" 188*760c253cSXin Li elfset2 = {e.replace(t2, "") for e in i2.elf_files} 189*760c253cSXin Li dif1 = elfset1.difference(elfset2) 190*760c253cSXin Li msg = None 191*760c253cSXin Li if dif1: 192*760c253cSXin Li msg = 'The following files are not in "{image}" - "{rootfs}":\n'.format( 193*760c253cSXin Li image=i2.image, rootfs=i2.rootfs 194*760c253cSXin Li ) 195*760c253cSXin Li for d in dif1: 196*760c253cSXin Li msg += "\t" + d + "\n" 197*760c253cSXin Li dif2 = elfset2.difference(elfset1) 198*760c253cSXin Li if dif2: 199*760c253cSXin Li msg = 'The following files are not in "{image}" - "{rootfs}":\n'.format( 200*760c253cSXin Li image=i1.image, rootfs=i1.rootfs 201*760c253cSXin Li ) 202*760c253cSXin Li for d in dif2: 203*760c253cSXin Li msg += "\t" + d + "\n" 204*760c253cSXin Li if msg: 205*760c253cSXin Li self.logger.LogError(msg) 206*760c253cSXin Li return False 207*760c253cSXin Li return True 208*760c253cSXin Li 209*760c253cSXin Li def CompareImages(self): 210*760c253cSXin Li """Do the comparsion work.""" 211*760c253cSXin Li 212*760c253cSXin Li if not self.CheckElfFileSetEquality(): 213*760c253cSXin Li return False 214*760c253cSXin Li 215*760c253cSXin Li mismatch_list = [] 216*760c253cSXin Li match_count = 0 217*760c253cSXin Li i1 = self.images[0] 218*760c253cSXin Li i2 = self.images[1] 219*760c253cSXin Li self.logger.LogOutput( 220*760c253cSXin Li "Start comparing {0} elf file by file ...".format(len(i1.elf_files)) 221*760c253cSXin Li ) 222*760c253cSXin Li ## Note - i1.elf_files and i2.elf_files have exactly the same entries here. 223*760c253cSXin Li 224*760c253cSXin Li ## Create 2 temp files to be used for all disassembed files. 225*760c253cSXin Li handle, self.tempf1 = tempfile.mkstemp() 226*760c253cSXin Li os.close(handle) # We do not need the handle 227*760c253cSXin Li handle, self.tempf2 = tempfile.mkstemp() 228*760c253cSXin Li os.close(handle) 229*760c253cSXin Li 230*760c253cSXin Li cmde = command_executer.GetCommandExecuter() 231*760c253cSXin Li for elf1 in i1.elf_files: 232*760c253cSXin Li tmp_rootfs = i1.rootfs + "/" 233*760c253cSXin Li f1 = elf1.replace(tmp_rootfs, "") 234*760c253cSXin Li full_path1 = elf1 235*760c253cSXin Li full_path2 = elf1.replace(i1.rootfs, i2.rootfs) 236*760c253cSXin Li 237*760c253cSXin Li if full_path1 == full_path2: 238*760c253cSXin Li self.logger.LogError( 239*760c253cSXin Li "Error: We're comparing the SAME file - {0}".format(f1) 240*760c253cSXin Li ) 241*760c253cSXin Li continue 242*760c253cSXin Li 243*760c253cSXin Li command = ( 244*760c253cSXin Li 'objdump -d "{f1}" > {tempf1} ; ' 245*760c253cSXin Li 'objdump -d "{f2}" > {tempf2} ; ' 246*760c253cSXin Li # Remove path string inside the dissemble 247*760c253cSXin Li "sed -i 's!{rootfs1}!!g' {tempf1} ; " 248*760c253cSXin Li "sed -i 's!{rootfs2}!!g' {tempf2} ; " 249*760c253cSXin Li "diff {tempf1} {tempf2} 1>/dev/null 2>&1" 250*760c253cSXin Li ).format( 251*760c253cSXin Li f1=full_path1, 252*760c253cSXin Li f2=full_path2, 253*760c253cSXin Li rootfs1=i1.rootfs, 254*760c253cSXin Li rootfs2=i2.rootfs, 255*760c253cSXin Li tempf1=self.tempf1, 256*760c253cSXin Li tempf2=self.tempf2, 257*760c253cSXin Li ) 258*760c253cSXin Li ret = cmde.RunCommand(command, print_to_console=False) 259*760c253cSXin Li if ret != 0: 260*760c253cSXin Li self.logger.LogOutput( 261*760c253cSXin Li '*** Not match - "{0}" "{1}"'.format(full_path1, full_path2) 262*760c253cSXin Li ) 263*760c253cSXin Li mismatch_list.append(f1) 264*760c253cSXin Li if self.diff_file: 265*760c253cSXin Li command = ( 266*760c253cSXin Li 'echo "Diffs of disassemble of "{f1}" and "{f2}"" ' 267*760c253cSXin Li ">> {diff_file} ; diff {tempf1} {tempf2} " 268*760c253cSXin Li ">> {diff_file}" 269*760c253cSXin Li ).format( 270*760c253cSXin Li f1=full_path1, 271*760c253cSXin Li f2=full_path2, 272*760c253cSXin Li diff_file=self.diff_file, 273*760c253cSXin Li tempf1=self.tempf1, 274*760c253cSXin Li tempf2=self.tempf2, 275*760c253cSXin Li ) 276*760c253cSXin Li cmde.RunCommand(command, print_to_console=False) 277*760c253cSXin Li else: 278*760c253cSXin Li match_count += 1 279*760c253cSXin Li ## End of comparing every elf files. 280*760c253cSXin Li 281*760c253cSXin Li if not mismatch_list: 282*760c253cSXin Li self.logger.LogOutput( 283*760c253cSXin Li "** COOL, ALL {0} BINARIES MATCHED!! **".format(match_count) 284*760c253cSXin Li ) 285*760c253cSXin Li return True 286*760c253cSXin Li 287*760c253cSXin Li mismatch_str = "Found {0} mismatch:\n".format(len(mismatch_list)) 288*760c253cSXin Li for b in mismatch_list: 289*760c253cSXin Li mismatch_str += "\t" + b + "\n" 290*760c253cSXin Li 291*760c253cSXin Li self.logger.LogOutput(mismatch_str) 292*760c253cSXin Li return False 293*760c253cSXin Li 294*760c253cSXin Li 295*760c253cSXin Lidef Main(argv): 296*760c253cSXin Li """The main function.""" 297*760c253cSXin Li 298*760c253cSXin Li command_executer.InitCommandExecuter() 299*760c253cSXin Li images = [] 300*760c253cSXin Li 301*760c253cSXin Li parser = argparse.ArgumentParser() 302*760c253cSXin Li parser.add_argument( 303*760c253cSXin Li "--no_unmount", 304*760c253cSXin Li action="store_true", 305*760c253cSXin Li dest="no_unmount", 306*760c253cSXin Li default=False, 307*760c253cSXin Li help="Do not unmount after finish, this is useful for debugging.", 308*760c253cSXin Li ) 309*760c253cSXin Li parser.add_argument( 310*760c253cSXin Li "--chromeos_root", 311*760c253cSXin Li dest="chromeos_root", 312*760c253cSXin Li default=None, 313*760c253cSXin Li action="store", 314*760c253cSXin Li help=( 315*760c253cSXin Li "[Optional] Specify a chromeos tree instead of " 316*760c253cSXin Li "deducing it from image path so that we can compare " 317*760c253cSXin Li "2 images that are downloaded." 318*760c253cSXin Li ), 319*760c253cSXin Li ) 320*760c253cSXin Li parser.add_argument( 321*760c253cSXin Li "--mount_basename", 322*760c253cSXin Li dest="mount_basename", 323*760c253cSXin Li default=None, 324*760c253cSXin Li action="store", 325*760c253cSXin Li help=( 326*760c253cSXin Li "Specify a meaningful name for the mount point. With this being " 327*760c253cSXin Li 'set, the mount points would be "/tmp/mount_basename.x.rootfs" ' 328*760c253cSXin Li ' and "/tmp/mount_basename.x.stateful". (x is 1 or 2).' 329*760c253cSXin Li ), 330*760c253cSXin Li ) 331*760c253cSXin Li parser.add_argument( 332*760c253cSXin Li "--diff_file", 333*760c253cSXin Li dest="diff_file", 334*760c253cSXin Li default=None, 335*760c253cSXin Li help="Dumping all the diffs (if any) to the diff file", 336*760c253cSXin Li ) 337*760c253cSXin Li parser.add_argument( 338*760c253cSXin Li "--image1", 339*760c253cSXin Li dest="image1", 340*760c253cSXin Li default=None, 341*760c253cSXin Li required=True, 342*760c253cSXin Li help=("Image 1 file name."), 343*760c253cSXin Li ) 344*760c253cSXin Li parser.add_argument( 345*760c253cSXin Li "--image2", 346*760c253cSXin Li dest="image2", 347*760c253cSXin Li default=None, 348*760c253cSXin Li required=True, 349*760c253cSXin Li help=("Image 2 file name."), 350*760c253cSXin Li ) 351*760c253cSXin Li options = parser.parse_args(argv[1:]) 352*760c253cSXin Li 353*760c253cSXin Li if options.mount_basename and options.mount_basename.find("/") >= 0: 354*760c253cSXin Li logger.GetLogger().LogError( 355*760c253cSXin Li '"--mount_basename" must be a name, not a path.' 356*760c253cSXin Li ) 357*760c253cSXin Li parser.print_help() 358*760c253cSXin Li return 1 359*760c253cSXin Li 360*760c253cSXin Li result = False 361*760c253cSXin Li image_comparator = None 362*760c253cSXin Li try: 363*760c253cSXin Li for i, image_path in enumerate( 364*760c253cSXin Li [options.image1, options.image2], start=1 365*760c253cSXin Li ): 366*760c253cSXin Li image_path = os.path.realpath(image_path) 367*760c253cSXin Li if not os.path.isfile(image_path): 368*760c253cSXin Li logger.GetLogger().LogError( 369*760c253cSXin Li '"{0}" is not a file.'.format(image_path) 370*760c253cSXin Li ) 371*760c253cSXin Li return 1 372*760c253cSXin Li 373*760c253cSXin Li chromeos_root = None 374*760c253cSXin Li if options.chromeos_root: 375*760c253cSXin Li chromeos_root = options.chromeos_root 376*760c253cSXin Li else: 377*760c253cSXin Li ## Deduce chromeos root from image 378*760c253cSXin Li t = image_path 379*760c253cSXin Li while t != "/": 380*760c253cSXin Li if misc.IsChromeOsTree(t): 381*760c253cSXin Li break 382*760c253cSXin Li t = os.path.dirname(t) 383*760c253cSXin Li if misc.IsChromeOsTree(t): 384*760c253cSXin Li chromeos_root = t 385*760c253cSXin Li 386*760c253cSXin Li if not chromeos_root: 387*760c253cSXin Li logger.GetLogger().LogError( 388*760c253cSXin Li "Please provide a valid chromeos root via --chromeos_root" 389*760c253cSXin Li ) 390*760c253cSXin Li return 1 391*760c253cSXin Li 392*760c253cSXin Li image = CrosImage(image_path, chromeos_root, options.no_unmount) 393*760c253cSXin Li 394*760c253cSXin Li if options.mount_basename: 395*760c253cSXin Li mount_basename = "{basename}.{index}".format( 396*760c253cSXin Li basename=options.mount_basename, index=i 397*760c253cSXin Li ) 398*760c253cSXin Li else: 399*760c253cSXin Li mount_basename = None 400*760c253cSXin Li 401*760c253cSXin Li if image.MountImage(mount_basename): 402*760c253cSXin Li images.append(image) 403*760c253cSXin Li image.FindElfFiles() 404*760c253cSXin Li 405*760c253cSXin Li if len(images) == 2: 406*760c253cSXin Li image_comparator = ImageComparator(images, options.diff_file) 407*760c253cSXin Li result = image_comparator.CompareImages() 408*760c253cSXin Li finally: 409*760c253cSXin Li for image in images: 410*760c253cSXin Li image.UnmountImage() 411*760c253cSXin Li if image_comparator: 412*760c253cSXin Li image_comparator.Cleanup() 413*760c253cSXin Li 414*760c253cSXin Li return 0 if result else 1 415*760c253cSXin Li 416*760c253cSXin Li 417*760c253cSXin Liif __name__ == "__main__": 418*760c253cSXin Li Main(sys.argv) 419