# Lint as: python2, python3 # 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 = 'sha256' 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.warning("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, ignore_status=True) lsblk = utils.system_output('lsblk %s' % (device), retain_output=True, ignore_status=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) 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 as 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.warning("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 as 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() def mod_nothing(self, run_count, backing_path, block_size, block_count): """Example callback for mod_and_test that does nothing.""" 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