Home | History | Annotate | Download | only in cros
      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 
      5 import logging, mmap, os, time
      6 
      7 import common
      8 from autotest_lib.client.bin import os_dep, test
      9 from 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.
     14 ERROR_BEHAVIOR_ERROR = 'eio'
     15 ERROR_BEHAVIOR_REBOOT = 'panic'
     16 ERROR_BEHAVIOR_IGNORE = 'none'
     17 ERROR_BEHAVIOR_NOTIFIER = 'notify'  # for platform specific behavior.
     18 
     19 # Default configuration for verity_image
     20 DEFAULT_TARGET_NAME = 'verity_image'
     21 DEFAULT_ALG = 'sha1'
     22 DEFAULT_IMAGE_SIZE_IN_BLOCKS = 100
     23 DEFAULT_ERROR_BEHAVIOR = ERROR_BEHAVIOR_ERROR
     24 # TODO(wad) make this configurable when dm-verity doesn't hard-code 4096.
     25 BLOCK_SIZE = 4096
     26 
     27 def 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 
     38 class 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 
    247 class 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