xref: /aosp_15_r20/external/autotest/client/cros/verity_utils.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1*9c5db199SXin Li# Lint as: python2, python3
2*9c5db199SXin Li# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
3*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be
4*9c5db199SXin Li# found in the LICENSE file.
5*9c5db199SXin Li
6*9c5db199SXin Liimport logging, mmap, os, time
7*9c5db199SXin Li
8*9c5db199SXin Liimport common
9*9c5db199SXin Lifrom autotest_lib.client.bin import os_dep, test
10*9c5db199SXin Lifrom autotest_lib.client.common_lib import error, logging_manager, utils
11*9c5db199SXin Li
12*9c5db199SXin Li""" a wrapper for using verity/dm-verity with a test backing store """
13*9c5db199SXin Li
14*9c5db199SXin Li# enum for the 3 possible values of the module parameter.
15*9c5db199SXin LiERROR_BEHAVIOR_ERROR = 'eio'
16*9c5db199SXin LiERROR_BEHAVIOR_REBOOT = 'panic'
17*9c5db199SXin LiERROR_BEHAVIOR_IGNORE = 'none'
18*9c5db199SXin LiERROR_BEHAVIOR_NOTIFIER = 'notify'  # for platform specific behavior.
19*9c5db199SXin Li
20*9c5db199SXin Li# Default configuration for verity_image
21*9c5db199SXin LiDEFAULT_TARGET_NAME = 'verity_image'
22*9c5db199SXin LiDEFAULT_ALG = 'sha256'
23*9c5db199SXin LiDEFAULT_IMAGE_SIZE_IN_BLOCKS = 100
24*9c5db199SXin LiDEFAULT_ERROR_BEHAVIOR = ERROR_BEHAVIOR_ERROR
25*9c5db199SXin Li# TODO(wad) make this configurable when dm-verity doesn't hard-code 4096.
26*9c5db199SXin LiBLOCK_SIZE = 4096
27*9c5db199SXin Li
28*9c5db199SXin Lidef system(command, timeout=None):
29*9c5db199SXin Li    """Delegate to utils.system to run |command|, logs stderr only on fail.
30*9c5db199SXin Li
31*9c5db199SXin Li    Runs |command|, captures stdout and stderr.  Logs stdout to the DEBUG
32*9c5db199SXin Li    log no matter what, logs stderr only if the command actually fails.
33*9c5db199SXin Li    Will time the command out after |timeout|.
34*9c5db199SXin Li    """
35*9c5db199SXin Li    utils.run(command, timeout=timeout, ignore_status=False,
36*9c5db199SXin Li              stdout_tee=utils.TEE_TO_LOGS, stderr_tee=utils.TEE_TO_LOGS,
37*9c5db199SXin Li              stderr_is_expected=True)
38*9c5db199SXin Li
39*9c5db199SXin Liclass verity_image(object):
40*9c5db199SXin Li    """ a helper for creating dm-verity targets for testing.
41*9c5db199SXin Li
42*9c5db199SXin Li        To use,
43*9c5db199SXin Li          vi = verity_image()
44*9c5db199SXin Li          vi.initialize(self.tmpdir, "dmveritytesta")
45*9c5db199SXin Li          # Create a 409600 byte image with /bin/ls on it
46*9c5db199SXin Li          # The size in bytes is returned.
47*9c5db199SXin Li          backing_path = vi.create_backing_image(100, copy_files=['/bin/ls'])
48*9c5db199SXin Li          # Performs hashing of the backing_path and sets up a device.
49*9c5db199SXin Li          loop_dev = vi.prepare_backing_device()
50*9c5db199SXin Li          # Sets up the mapped device and returns the path:
51*9c5db199SXin Li          # E.g., /dev/mapper/autotest_dmveritytesta
52*9c5db199SXin Li          dev = vi.create_verity_device()
53*9c5db199SXin Li          # Access the mapped device using the returned string.
54*9c5db199SXin Li
55*9c5db199SXin Li       TODO(wad) add direct verified and backing store access functions
56*9c5db199SXin Li                 to make writing modifiers easier (e.g., mmap).
57*9c5db199SXin Li    """
58*9c5db199SXin Li    # Define the command template constants.
59*9c5db199SXin Li    verity_cmd = \
60*9c5db199SXin Li        'verity mode=create alg=%s payload=%s payload_blocks=%d hashtree=%s'
61*9c5db199SXin Li    dd_cmd = 'dd if=/dev/zero of=%s bs=4096 count=0 seek=%d'
62*9c5db199SXin Li    mkfs_cmd = 'mkfs.ext3 -b 4096 -F %s'
63*9c5db199SXin Li    dmsetup_cmd = "dmsetup -r create autotest_%s --table '%s'"
64*9c5db199SXin Li
65*9c5db199SXin Li    def _device_release(self, cmd, device):
66*9c5db199SXin Li        if utils.system(cmd, ignore_status=True) == 0:
67*9c5db199SXin Li            return
68*9c5db199SXin Li        logging.warning("Could not release %s. Retrying...", device)
69*9c5db199SXin Li        # Other things (like cros-disks) may have the device open briefly,
70*9c5db199SXin Li        # so if we initially fail, try again and attempt to gather details
71*9c5db199SXin Li        # on who else is using the device.
72*9c5db199SXin Li        fuser = utils.system_output('fuser -v %s' % (device),
73*9c5db199SXin Li                                    retain_output=True,
74*9c5db199SXin Li                                    ignore_status=True)
75*9c5db199SXin Li        lsblk = utils.system_output('lsblk %s' % (device),
76*9c5db199SXin Li                                    retain_output=True,
77*9c5db199SXin Li                                    ignore_status=True)
78*9c5db199SXin Li        time.sleep(1)
79*9c5db199SXin Li        if utils.system(cmd, ignore_status=True) == 0:
80*9c5db199SXin Li            return
81*9c5db199SXin Li        raise error.TestFail('"%s" failed: %s\n%s' % (cmd, fuser, lsblk))
82*9c5db199SXin Li
83*9c5db199SXin Li    def reset(self):
84*9c5db199SXin Li        """Idempotent call which will free any claimed system resources"""
85*9c5db199SXin Li        # Pre-initialize these values to None
86*9c5db199SXin Li        for attr in ['mountpoint', 'device', 'loop', 'file', 'hash_file']:
87*9c5db199SXin Li            if not hasattr(self, attr):
88*9c5db199SXin Li                setattr(self, attr, None)
89*9c5db199SXin Li        logging.info("verity_image is being reset")
90*9c5db199SXin Li
91*9c5db199SXin Li        if self.mountpoint is not None:
92*9c5db199SXin Li            system('umount %s' % self.mountpoint)
93*9c5db199SXin Li            self.mountpoint = None
94*9c5db199SXin Li
95*9c5db199SXin Li        if self.device is not None:
96*9c5db199SXin Li            self._device_release('dmsetup remove %s' % (self.device),
97*9c5db199SXin Li                                 self.device)
98*9c5db199SXin Li            self.device = None
99*9c5db199SXin Li
100*9c5db199SXin Li        if self.loop is not None:
101*9c5db199SXin Li            self._device_release('losetup -d %s' % (self.loop), self.loop)
102*9c5db199SXin Li            self.loop = None
103*9c5db199SXin Li
104*9c5db199SXin Li        if self.file is not None:
105*9c5db199SXin Li            os.remove(self.file)
106*9c5db199SXin Li            self.file = None
107*9c5db199SXin Li
108*9c5db199SXin Li        if self.hash_file is not None:
109*9c5db199SXin Li            os.remove(self.hash_file)
110*9c5db199SXin Li            self.hash_file = None
111*9c5db199SXin Li
112*9c5db199SXin Li        self.alg = DEFAULT_ALG
113*9c5db199SXin Li        self.error_behavior = DEFAULT_ERROR_BEHAVIOR
114*9c5db199SXin Li        self.blocks = DEFAULT_IMAGE_SIZE_IN_BLOCKS
115*9c5db199SXin Li        self.file = None
116*9c5db199SXin Li        self.has_fs = False
117*9c5db199SXin Li        self.hash_file = None
118*9c5db199SXin Li        self.table = None
119*9c5db199SXin Li        self.target_name = DEFAULT_TARGET_NAME
120*9c5db199SXin Li
121*9c5db199SXin Li        self.__initialized = False
122*9c5db199SXin Li
123*9c5db199SXin Li    def __init__(self):
124*9c5db199SXin Li        """Sets up the defaults for the object and then calls reset()
125*9c5db199SXin Li        """
126*9c5db199SXin Li        self.reset()
127*9c5db199SXin Li
128*9c5db199SXin Li    def __del__(self):
129*9c5db199SXin Li        # Release any and all system resources.
130*9c5db199SXin Li        self.reset()
131*9c5db199SXin Li
132*9c5db199SXin Li    def _create_image(self):
133*9c5db199SXin Li        """Creates a placeholder file."""
134*9c5db199SXin Li        # TODO(wad) replace with python
135*9c5db199SXin Li        utils.system_output(self.dd_cmd % (self.file, self.blocks))
136*9c5db199SXin Li
137*9c5db199SXin Li    def _create_fs(self, copy_files):
138*9c5db199SXin Li        """sets up ext3 on the image"""
139*9c5db199SXin Li        self.has_fs = True
140*9c5db199SXin Li        system(self.mkfs_cmd % self.file)
141*9c5db199SXin Li
142*9c5db199SXin Li    def _hash_image(self):
143*9c5db199SXin Li        """runs verity over the image and saves the device mapper table"""
144*9c5db199SXin Li        self.table = utils.system_output(self.verity_cmd % (self.alg,
145*9c5db199SXin Li                                                            self.file,
146*9c5db199SXin Li                                                            self.blocks,
147*9c5db199SXin Li                                                            self.hash_file))
148*9c5db199SXin Li        # The verity tool doesn't include a templated error value.
149*9c5db199SXin Li        # For now, we add one.
150*9c5db199SXin Li        self.table += " error_behavior=ERROR_BEHAVIOR"
151*9c5db199SXin Li        logging.info("table is %s", self.table)
152*9c5db199SXin Li
153*9c5db199SXin Li    def _append_hash(self):
154*9c5db199SXin Li        f = open(self.file, 'ab')
155*9c5db199SXin Li        f.write(utils.read_file(self.hash_file))
156*9c5db199SXin Li        f.close()
157*9c5db199SXin Li
158*9c5db199SXin Li    def _setup_loop(self):
159*9c5db199SXin Li        # Setup a loop device
160*9c5db199SXin Li        self.loop = utils.system_output('losetup -f --show %s' % (self.file))
161*9c5db199SXin Li
162*9c5db199SXin Li    def _setup_target(self):
163*9c5db199SXin Li        # Update the table with the loop dev
164*9c5db199SXin Li        self.table = self.table.replace('HASH_DEV', self.loop)
165*9c5db199SXin Li        self.table = self.table.replace('ROOT_DEV', self.loop)
166*9c5db199SXin Li        self.table = self.table.replace('ERROR_BEHAVIOR', self.error_behavior)
167*9c5db199SXin Li
168*9c5db199SXin Li        system(self.dmsetup_cmd % (self.target_name, self.table))
169*9c5db199SXin Li        self.device = "/dev/mapper/autotest_%s" % self.target_name
170*9c5db199SXin Li
171*9c5db199SXin Li    def initialize(self,
172*9c5db199SXin Li                   tmpdir,
173*9c5db199SXin Li                   target_name,
174*9c5db199SXin Li                   alg=DEFAULT_ALG,
175*9c5db199SXin Li                   size_in_blocks=DEFAULT_IMAGE_SIZE_IN_BLOCKS,
176*9c5db199SXin Li                   error_behavior=DEFAULT_ERROR_BEHAVIOR):
177*9c5db199SXin Li        """Performs any required system-level initialization before use.
178*9c5db199SXin Li        """
179*9c5db199SXin Li        try:
180*9c5db199SXin Li            os_dep.commands('losetup', 'mkfs.ext3', 'dmsetup', 'verity', 'dd',
181*9c5db199SXin Li                            'dumpe2fs')
182*9c5db199SXin Li        except ValueError as e:
183*9c5db199SXin Li            logging.error('verity_image cannot be used without: %s', e)
184*9c5db199SXin Li            return False
185*9c5db199SXin Li
186*9c5db199SXin Li        # Used for the mapper device name and the tmpfile names.
187*9c5db199SXin Li        self.target_name = target_name
188*9c5db199SXin Li
189*9c5db199SXin Li        # Reserve some files to use.
190*9c5db199SXin Li        self.file = os.tempnam(tmpdir, '%s.img.' % self.target_name)
191*9c5db199SXin Li        self.hash_file = os.tempnam(tmpdir, '%s.hash.' % self.target_name)
192*9c5db199SXin Li
193*9c5db199SXin Li        # Set up the configurable bits.
194*9c5db199SXin Li        self.alg = alg
195*9c5db199SXin Li        self.error_behavior = error_behavior
196*9c5db199SXin Li        self.blocks = size_in_blocks
197*9c5db199SXin Li
198*9c5db199SXin Li        self.__initialized = True
199*9c5db199SXin Li        return True
200*9c5db199SXin Li
201*9c5db199SXin Li    def create_backing_image(self, size_in_blocks, with_fs=True,
202*9c5db199SXin Li                             copy_files=None):
203*9c5db199SXin Li        """Creates an image file of the given number of blocks and if specified
204*9c5db199SXin Li           will create a filesystem and copy any files in a copy_files list to
205*9c5db199SXin Li           the fs.
206*9c5db199SXin Li        """
207*9c5db199SXin Li        self.blocks = size_in_blocks
208*9c5db199SXin Li        self._create_image()
209*9c5db199SXin Li
210*9c5db199SXin Li        if with_fs is True:
211*9c5db199SXin Li            self._create_fs(copy_files)
212*9c5db199SXin Li        else:
213*9c5db199SXin Li            if type(copy_files) is list and len(copy_files) != 0:
214*9c5db199SXin Li                logging.warning("verity_image.initialize called with " \
215*9c5db199SXin Li                             "files to copy but no fs")
216*9c5db199SXin Li
217*9c5db199SXin Li        return self.file
218*9c5db199SXin Li
219*9c5db199SXin Li    def prepare_backing_device(self):
220*9c5db199SXin Li        """Hashes the backing image, appends it to the backing image, points
221*9c5db199SXin Li           a loop device at it and returns the path to the loop."""
222*9c5db199SXin Li        self._hash_image()
223*9c5db199SXin Li        self._append_hash()
224*9c5db199SXin Li        self._setup_loop()
225*9c5db199SXin Li        return self.loop
226*9c5db199SXin Li
227*9c5db199SXin Li    def create_verity_device(self):
228*9c5db199SXin Li        """Sets up the device mapper node and returns its path"""
229*9c5db199SXin Li        self._setup_target()
230*9c5db199SXin Li        return self.device
231*9c5db199SXin Li
232*9c5db199SXin Li    def verifiable(self):
233*9c5db199SXin Li        """Returns True if the dm-verity device does not throw any errors
234*9c5db199SXin Li           when being walked completely or False if it does."""
235*9c5db199SXin Li        try:
236*9c5db199SXin Li            if self.has_fs is True:
237*9c5db199SXin Li                system('dumpe2fs %s' % self.device)
238*9c5db199SXin Li            # TODO(wad) replace with mmap.mmap-based access
239*9c5db199SXin Li            system('dd if=%s of=/dev/null bs=4096' % self.device)
240*9c5db199SXin Li            return True
241*9c5db199SXin Li        except error.CmdError as e:
242*9c5db199SXin Li            return False
243*9c5db199SXin Li
244*9c5db199SXin Li
245*9c5db199SXin Liclass VerityImageTest(test.test):
246*9c5db199SXin Li    """VerityImageTest provides a base class for verity_image tests
247*9c5db199SXin Li       to be derived from.  It sets up a verity_image object for use
248*9c5db199SXin Li       and provides the function mod_and_test() to wrap simple test
249*9c5db199SXin Li       cases for verity_images.
250*9c5db199SXin Li
251*9c5db199SXin Li       See platform_DMVerityCorruption as an example usage.
252*9c5db199SXin Li    """
253*9c5db199SXin Li    version = 1
254*9c5db199SXin Li    image_blocks = DEFAULT_IMAGE_SIZE_IN_BLOCKS
255*9c5db199SXin Li
256*9c5db199SXin Li    def initialize(self):
257*9c5db199SXin Li        """Overrides test.initialize() to setup a verity_image"""
258*9c5db199SXin Li        self.verity = verity_image()
259*9c5db199SXin Li
260*9c5db199SXin Li    def mod_nothing(self, run_count, backing_path, block_size, block_count):
261*9c5db199SXin Li        """Example callback for mod_and_test that does nothing."""
262*9c5db199SXin Li        pass
263*9c5db199SXin Li
264*9c5db199SXin Li    def mod_and_test(self, modifier, count, expected):
265*9c5db199SXin Li        """Takes in a callback |modifier| and runs it |count| times over
266*9c5db199SXin Li           the verified image checking for |expected| out of verity.verifiable()
267*9c5db199SXin Li        """
268*9c5db199SXin Li        tries = 0
269*9c5db199SXin Li        while tries < count:
270*9c5db199SXin Li            # Start fresh then modify each block in the image.
271*9c5db199SXin Li            self.verity.reset()
272*9c5db199SXin Li            self.verity.initialize(self.tmpdir, self.__class__.__name__)
273*9c5db199SXin Li            backing_path = self.verity.create_backing_image(self.image_blocks)
274*9c5db199SXin Li            loop_dev = self.verity.prepare_backing_device()
275*9c5db199SXin Li
276*9c5db199SXin Li            modifier(tries,
277*9c5db199SXin Li                     backing_path,
278*9c5db199SXin Li                     BLOCK_SIZE,
279*9c5db199SXin Li                     self.image_blocks)
280*9c5db199SXin Li
281*9c5db199SXin Li            mapped_dev = self.verity.create_verity_device()
282*9c5db199SXin Li
283*9c5db199SXin Li            # Now check for failure.
284*9c5db199SXin Li            if self.verity.verifiable() is not expected:
285*9c5db199SXin Li                raise error.TestFail(
286*9c5db199SXin Li                    '%s: verity.verifiable() not as expected (%s)' %
287*9c5db199SXin Li                    (modifier.__name__, expected))
288*9c5db199SXin Li            tries += 1
289