xref: /aosp_15_r20/external/toolchain-utils/chromiumos_image_diff.py (revision 760c253c1ed00ce9abd48f8546f08516e57485fe)
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