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