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