| # Copyright (c) 2012 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import logging, mmap, os, time |
| |
| import common |
| from autotest_lib.client.bin import os_dep, test |
| from autotest_lib.client.common_lib import error, logging_manager, utils |
| |
| """ a wrapper for using verity/dm-verity with a test backing store """ |
| |
| # enum for the 3 possible values of the module parameter. |
| ERROR_BEHAVIOR_ERROR = 'eio' |
| ERROR_BEHAVIOR_REBOOT = 'panic' |
| ERROR_BEHAVIOR_IGNORE = 'none' |
| ERROR_BEHAVIOR_NOTIFIER = 'notify' # for platform specific behavior. |
| |
| # Default configuration for verity_image |
| DEFAULT_TARGET_NAME = 'verity_image' |
| DEFAULT_ALG = 'sha1' |
| DEFAULT_IMAGE_SIZE_IN_BLOCKS = 100 |
| DEFAULT_ERROR_BEHAVIOR = ERROR_BEHAVIOR_ERROR |
| # TODO(wad) make this configurable when dm-verity doesn't hard-code 4096. |
| BLOCK_SIZE = 4096 |
| |
| def system(command, timeout=None): |
| """Delegate to utils.system to run |command|, logs stderr only on fail. |
| |
| Runs |command|, captures stdout and stderr. Logs stdout to the DEBUG |
| log no matter what, logs stderr only if the command actually fails. |
| Will time the command out after |timeout|. |
| """ |
| utils.run(command, timeout=timeout, ignore_status=False, |
| stdout_tee=utils.TEE_TO_LOGS, stderr_tee=utils.TEE_TO_LOGS, |
| stderr_is_expected=True) |
| |
| class verity_image(object): |
| """ a helper for creating dm-verity targets for testing. |
| |
| To use, |
| vi = verity_image() |
| vi.initialize(self.tmpdir, "dmveritytesta") |
| # Create a 409600 byte image with /bin/ls on it |
| # The size in bytes is returned. |
| backing_path = vi.create_backing_image(100, copy_files=['/bin/ls']) |
| # Performs hashing of the backing_path and sets up a device. |
| loop_dev = vi.prepare_backing_device() |
| # Sets up the mapped device and returns the path: |
| # E.g., /dev/mapper/autotest_dmveritytesta |
| dev = vi.create_verity_device() |
| # Access the mapped device using the returned string. |
| |
| TODO(wad) add direct verified and backing store access functions |
| to make writing modifiers easier (e.g., mmap). |
| """ |
| # Define the command template constants. |
| verity_cmd = \ |
| 'verity mode=create alg=%s payload=%s payload_blocks=%d hashtree=%s' |
| dd_cmd = 'dd if=/dev/zero of=%s bs=4096 count=0 seek=%d' |
| mkfs_cmd = 'mkfs.ext3 -b 4096 -F %s' |
| dmsetup_cmd = "dmsetup -r create autotest_%s --table '%s'" |
| |
| def _device_release(self, cmd, device): |
| if utils.system(cmd, ignore_status=True) == 0: |
| return |
| logging.warn("Could not release %s. Retrying..." % (device)) |
| # Other things (like cros-disks) may have the device open briefly, |
| # so if we initially fail, try again and attempt to gather details |
| # on who else is using the device. |
| fuser = utils.system_output('fuser -v %s' % (device), |
| retain_output=True) |
| lsblk = utils.system_output('lsblk %s' % (device), |
| retain_output=True) |
| time.sleep(1) |
| if utils.system(cmd, ignore_status=True) == 0: |
| return |
| raise error.TestFail('"%s" failed: %s\n%s' % (cmd, fuser, lsblk)) |
| |
| def reset(self): |
| """Idempotent call which will free any claimed system resources""" |
| # Pre-initialize these values to None |
| for attr in ['mountpoint', 'device', 'loop', 'file', 'hash_file']: |
| if not hasattr(self, attr): |
| setattr(self, attr, None) |
| logging.info("verity_image is being reset") |
| |
| if self.mountpoint is not None: |
| system('umount %s' % self.mountpoint) |
| self.mountpoint = None |
| |
| if self.device is not None: |
| self._device_release('dmsetup remove %s' % (self.device), |
| self.device) |
| self.device = None |
| |
| if self.loop is not None: |
| self._device_release('losetup -d %s' % (self.loop), self.loop) |
| self.loop = None |
| |
| if self.file is not None: |
| os.remove(self.file) |
| self.file = None |
| |
| if self.hash_file is not None: |
| os.remove(self.hash_file) |
| self.hash_file = None |
| |
| self.alg = DEFAULT_ALG |
| self.error_behavior = DEFAULT_ERROR_BEHAVIOR |
| self.blocks = DEFAULT_IMAGE_SIZE_IN_BLOCKS |
| self.file = None |
| self.has_fs = False |
| self.hash_file = None |
| self.table = None |
| self.target_name = DEFAULT_TARGET_NAME |
| |
| self.__initialized = False |
| |
| def __init__(self): |
| """Sets up the defaults for the object and then calls reset() |
| """ |
| self.reset() |
| |
| def __del__(self): |
| # Release any and all system resources. |
| self.reset() |
| |
| def _create_image(self): |
| """Creates a dummy file.""" |
| # TODO(wad) replace with python |
| utils.system_output(self.dd_cmd % (self.file, self.blocks)) |
| |
| def _create_fs(self, copy_files): |
| """sets up ext3 on the image""" |
| self.has_fs = True |
| system(self.mkfs_cmd % self.file) |
| if type(copy_files) is list: |
| for file in copy_files: |
| pass # TODO(wad) |
| |
| def _hash_image(self): |
| """runs verity over the image and saves the device mapper table""" |
| self.table = utils.system_output(self.verity_cmd % (self.alg, |
| self.file, |
| self.blocks, |
| self.hash_file)) |
| # The verity tool doesn't include a templated error value. |
| # For now, we add one. |
| self.table += " error_behavior=ERROR_BEHAVIOR" |
| logging.info("table is %s" % self.table) |
| |
| def _append_hash(self): |
| f = open(self.file, 'ab') |
| f.write(utils.read_file(self.hash_file)) |
| f.close() |
| |
| def _setup_loop(self): |
| # Setup a loop device |
| self.loop = utils.system_output('losetup -f --show %s' % (self.file)) |
| |
| def _setup_target(self): |
| # Update the table with the loop dev |
| self.table = self.table.replace('HASH_DEV', self.loop) |
| self.table = self.table.replace('ROOT_DEV', self.loop) |
| self.table = self.table.replace('ERROR_BEHAVIOR', self.error_behavior) |
| |
| system(self.dmsetup_cmd % (self.target_name, self.table)) |
| self.device = "/dev/mapper/autotest_%s" % self.target_name |
| |
| def initialize(self, |
| tmpdir, |
| target_name, |
| alg=DEFAULT_ALG, |
| size_in_blocks=DEFAULT_IMAGE_SIZE_IN_BLOCKS, |
| error_behavior=DEFAULT_ERROR_BEHAVIOR): |
| """Performs any required system-level initialization before use. |
| """ |
| try: |
| os_dep.commands('losetup', 'mkfs.ext3', 'dmsetup', 'verity', 'dd', |
| 'dumpe2fs') |
| except ValueError, e: |
| logging.error('verity_image cannot be used without: %s' % e) |
| return False |
| |
| # Used for the mapper device name and the tmpfile names. |
| self.target_name = target_name |
| |
| # Reserve some files to use. |
| self.file = os.tempnam(tmpdir, '%s.img.' % self.target_name) |
| self.hash_file = os.tempnam(tmpdir, '%s.hash.' % self.target_name) |
| |
| # Set up the configurable bits. |
| self.alg = alg |
| self.error_behavior = error_behavior |
| self.blocks = size_in_blocks |
| |
| self.__initialized = True |
| return True |
| |
| def create_backing_image(self, size_in_blocks, with_fs=True, |
| copy_files=None): |
| """Creates an image file of the given number of blocks and if specified |
| will create a filesystem and copy any files in a copy_files list to |
| the fs. |
| """ |
| self.blocks = size_in_blocks |
| self._create_image() |
| |
| if with_fs is True: |
| self._create_fs(copy_files) |
| else: |
| if type(copy_files) is list and len(copy_files) != 0: |
| logging.warn("verity_image.initialize called with " \ |
| "files to copy but no fs") |
| |
| return self.file |
| |
| def prepare_backing_device(self): |
| """Hashes the backing image, appends it to the backing image, points |
| a loop device at it and returns the path to the loop.""" |
| self._hash_image() |
| self._append_hash() |
| self._setup_loop() |
| return self.loop |
| |
| def create_verity_device(self): |
| """Sets up the device mapper node and returns its path""" |
| self._setup_target() |
| return self.device |
| |
| def verifiable(self): |
| """Returns True if the dm-verity device does not throw any errors |
| when being walked completely or False if it does.""" |
| try: |
| if self.has_fs is True: |
| system('dumpe2fs %s' % self.device) |
| # TODO(wad) replace with mmap.mmap-based access |
| system('dd if=%s of=/dev/null bs=4096' % self.device) |
| return True |
| except error.CmdError, e: |
| return False |
| |
| |
| class VerityImageTest(test.test): |
| """VerityImageTest provides a base class for verity_image tests |
| to be derived from. It sets up a verity_image object for use |
| and provides the function mod_and_test() to wrap simple test |
| cases for verity_images. |
| |
| See platform_DMVerityCorruption as an example usage. |
| """ |
| version = 1 |
| image_blocks = DEFAULT_IMAGE_SIZE_IN_BLOCKS |
| |
| def initialize(self): |
| """Overrides test.initialize() to setup a verity_image""" |
| self.verity = verity_image() |
| |
| # Example callback for mod_and_test that does nothing |
| def mod_nothing(self, run_count, backing_path, block_size, block_count): |
| pass |
| |
| def mod_and_test(self, modifier, count, expected): |
| """Takes in a callback |modifier| and runs it |count| times over |
| the verified image checking for |expected| out of verity.verifiable() |
| """ |
| tries = 0 |
| while tries < count: |
| # Start fresh then modify each block in the image. |
| self.verity.reset() |
| self.verity.initialize(self.tmpdir, self.__class__.__name__) |
| backing_path = self.verity.create_backing_image(self.image_blocks) |
| loop_dev = self.verity.prepare_backing_device() |
| |
| modifier(tries, |
| backing_path, |
| BLOCK_SIZE, |
| self.image_blocks) |
| |
| mapped_dev = self.verity.create_verity_device() |
| |
| # Now check for failure. |
| if self.verity.verifiable() is not expected: |
| raise error.TestFail( |
| '%s: verity.verifiable() not as expected (%s)' % |
| (modifier.__name__, expected)) |
| tries += 1 |