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