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