• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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