You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
289 lines
11 KiB
289 lines
11 KiB
# 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
|