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         lsblk = utils.system_output('lsblk %s' % (device),
     74                                     retain_output=True)
     75         time.sleep(1)
     76         if utils.system(cmd, ignore_status=True) == 0:
     77             return
     78         raise error.TestFail('"%s" failed: %s\n%s' % (cmd, fuser, lsblk))
     79 
     80     def reset(self):
     81         """Idempotent call which will free any claimed system resources"""
     82         # Pre-initialize these values to None
     83         for attr in ['mountpoint', 'device', 'loop', 'file', 'hash_file']:
     84             if not hasattr(self, attr):
     85                 setattr(self, attr, None)
     86         logging.info("verity_image is being reset")
     87 
     88         if self.mountpoint is not None:
     89             system('umount %s' % self.mountpoint)
     90             self.mountpoint = None
     91 
     92         if self.device is not None:
     93             self._device_release('dmsetup remove %s' % (self.device),
     94                                  self.device)
     95             self.device = None
     96 
     97         if self.loop is not None:
     98             self._device_release('losetup -d %s' % (self.loop), self.loop)
     99             self.loop = None
    100 
    101         if self.file is not None:
    102             os.remove(self.file)
    103             self.file = None
    104 
    105         if self.hash_file is not None:
    106             os.remove(self.hash_file)
    107             self.hash_file = None
    108 
    109         self.alg = DEFAULT_ALG
    110         self.error_behavior = DEFAULT_ERROR_BEHAVIOR
    111         self.blocks = DEFAULT_IMAGE_SIZE_IN_BLOCKS
    112         self.file = None
    113         self.has_fs = False
    114         self.hash_file = None
    115         self.table = None
    116         self.target_name = DEFAULT_TARGET_NAME
    117 
    118         self.__initialized = False
    119 
    120     def __init__(self):
    121         """Sets up the defaults for the object and then calls reset()
    122         """
    123         self.reset()
    124 
    125     def __del__(self):
    126         # Release any and all system resources.
    127         self.reset()
    128 
    129     def _create_image(self):
    130         """Creates a dummy file."""
    131         # TODO(wad) replace with python
    132         utils.system_output(self.dd_cmd % (self.file, self.blocks))
    133 
    134     def _create_fs(self, copy_files):
    135         """sets up ext3 on the image"""
    136         self.has_fs = True
    137         system(self.mkfs_cmd % self.file)
    138         if type(copy_files) is list:
    139           for file in copy_files:
    140               pass  # TODO(wad)
    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, 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, e:
    242             return False
    243 
    244 
    245 class 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     # Example callback for mod_and_test that does nothing
    261     def mod_nothing(self, run_count, backing_path, block_size, block_count):
    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