1*9c5db199SXin Li# Lint as: python2, python3 2*9c5db199SXin Li# Copyright (c) 2012 The Chromium OS Authors. All rights reserved. 3*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be 4*9c5db199SXin Li# found in the LICENSE file. 5*9c5db199SXin Li 6*9c5db199SXin Liimport logging, mmap, os, time 7*9c5db199SXin Li 8*9c5db199SXin Liimport common 9*9c5db199SXin Lifrom autotest_lib.client.bin import os_dep, test 10*9c5db199SXin Lifrom autotest_lib.client.common_lib import error, logging_manager, utils 11*9c5db199SXin Li 12*9c5db199SXin Li""" a wrapper for using verity/dm-verity with a test backing store """ 13*9c5db199SXin Li 14*9c5db199SXin Li# enum for the 3 possible values of the module parameter. 15*9c5db199SXin LiERROR_BEHAVIOR_ERROR = 'eio' 16*9c5db199SXin LiERROR_BEHAVIOR_REBOOT = 'panic' 17*9c5db199SXin LiERROR_BEHAVIOR_IGNORE = 'none' 18*9c5db199SXin LiERROR_BEHAVIOR_NOTIFIER = 'notify' # for platform specific behavior. 19*9c5db199SXin Li 20*9c5db199SXin Li# Default configuration for verity_image 21*9c5db199SXin LiDEFAULT_TARGET_NAME = 'verity_image' 22*9c5db199SXin LiDEFAULT_ALG = 'sha256' 23*9c5db199SXin LiDEFAULT_IMAGE_SIZE_IN_BLOCKS = 100 24*9c5db199SXin LiDEFAULT_ERROR_BEHAVIOR = ERROR_BEHAVIOR_ERROR 25*9c5db199SXin Li# TODO(wad) make this configurable when dm-verity doesn't hard-code 4096. 26*9c5db199SXin LiBLOCK_SIZE = 4096 27*9c5db199SXin Li 28*9c5db199SXin Lidef system(command, timeout=None): 29*9c5db199SXin Li """Delegate to utils.system to run |command|, logs stderr only on fail. 30*9c5db199SXin Li 31*9c5db199SXin Li Runs |command|, captures stdout and stderr. Logs stdout to the DEBUG 32*9c5db199SXin Li log no matter what, logs stderr only if the command actually fails. 33*9c5db199SXin Li Will time the command out after |timeout|. 34*9c5db199SXin Li """ 35*9c5db199SXin Li utils.run(command, timeout=timeout, ignore_status=False, 36*9c5db199SXin Li stdout_tee=utils.TEE_TO_LOGS, stderr_tee=utils.TEE_TO_LOGS, 37*9c5db199SXin Li stderr_is_expected=True) 38*9c5db199SXin Li 39*9c5db199SXin Liclass verity_image(object): 40*9c5db199SXin Li """ a helper for creating dm-verity targets for testing. 41*9c5db199SXin Li 42*9c5db199SXin Li To use, 43*9c5db199SXin Li vi = verity_image() 44*9c5db199SXin Li vi.initialize(self.tmpdir, "dmveritytesta") 45*9c5db199SXin Li # Create a 409600 byte image with /bin/ls on it 46*9c5db199SXin Li # The size in bytes is returned. 47*9c5db199SXin Li backing_path = vi.create_backing_image(100, copy_files=['/bin/ls']) 48*9c5db199SXin Li # Performs hashing of the backing_path and sets up a device. 49*9c5db199SXin Li loop_dev = vi.prepare_backing_device() 50*9c5db199SXin Li # Sets up the mapped device and returns the path: 51*9c5db199SXin Li # E.g., /dev/mapper/autotest_dmveritytesta 52*9c5db199SXin Li dev = vi.create_verity_device() 53*9c5db199SXin Li # Access the mapped device using the returned string. 54*9c5db199SXin Li 55*9c5db199SXin Li TODO(wad) add direct verified and backing store access functions 56*9c5db199SXin Li to make writing modifiers easier (e.g., mmap). 57*9c5db199SXin Li """ 58*9c5db199SXin Li # Define the command template constants. 59*9c5db199SXin Li verity_cmd = \ 60*9c5db199SXin Li 'verity mode=create alg=%s payload=%s payload_blocks=%d hashtree=%s' 61*9c5db199SXin Li dd_cmd = 'dd if=/dev/zero of=%s bs=4096 count=0 seek=%d' 62*9c5db199SXin Li mkfs_cmd = 'mkfs.ext3 -b 4096 -F %s' 63*9c5db199SXin Li dmsetup_cmd = "dmsetup -r create autotest_%s --table '%s'" 64*9c5db199SXin Li 65*9c5db199SXin Li def _device_release(self, cmd, device): 66*9c5db199SXin Li if utils.system(cmd, ignore_status=True) == 0: 67*9c5db199SXin Li return 68*9c5db199SXin Li logging.warning("Could not release %s. Retrying...", device) 69*9c5db199SXin Li # Other things (like cros-disks) may have the device open briefly, 70*9c5db199SXin Li # so if we initially fail, try again and attempt to gather details 71*9c5db199SXin Li # on who else is using the device. 72*9c5db199SXin Li fuser = utils.system_output('fuser -v %s' % (device), 73*9c5db199SXin Li retain_output=True, 74*9c5db199SXin Li ignore_status=True) 75*9c5db199SXin Li lsblk = utils.system_output('lsblk %s' % (device), 76*9c5db199SXin Li retain_output=True, 77*9c5db199SXin Li ignore_status=True) 78*9c5db199SXin Li time.sleep(1) 79*9c5db199SXin Li if utils.system(cmd, ignore_status=True) == 0: 80*9c5db199SXin Li return 81*9c5db199SXin Li raise error.TestFail('"%s" failed: %s\n%s' % (cmd, fuser, lsblk)) 82*9c5db199SXin Li 83*9c5db199SXin Li def reset(self): 84*9c5db199SXin Li """Idempotent call which will free any claimed system resources""" 85*9c5db199SXin Li # Pre-initialize these values to None 86*9c5db199SXin Li for attr in ['mountpoint', 'device', 'loop', 'file', 'hash_file']: 87*9c5db199SXin Li if not hasattr(self, attr): 88*9c5db199SXin Li setattr(self, attr, None) 89*9c5db199SXin Li logging.info("verity_image is being reset") 90*9c5db199SXin Li 91*9c5db199SXin Li if self.mountpoint is not None: 92*9c5db199SXin Li system('umount %s' % self.mountpoint) 93*9c5db199SXin Li self.mountpoint = None 94*9c5db199SXin Li 95*9c5db199SXin Li if self.device is not None: 96*9c5db199SXin Li self._device_release('dmsetup remove %s' % (self.device), 97*9c5db199SXin Li self.device) 98*9c5db199SXin Li self.device = None 99*9c5db199SXin Li 100*9c5db199SXin Li if self.loop is not None: 101*9c5db199SXin Li self._device_release('losetup -d %s' % (self.loop), self.loop) 102*9c5db199SXin Li self.loop = None 103*9c5db199SXin Li 104*9c5db199SXin Li if self.file is not None: 105*9c5db199SXin Li os.remove(self.file) 106*9c5db199SXin Li self.file = None 107*9c5db199SXin Li 108*9c5db199SXin Li if self.hash_file is not None: 109*9c5db199SXin Li os.remove(self.hash_file) 110*9c5db199SXin Li self.hash_file = None 111*9c5db199SXin Li 112*9c5db199SXin Li self.alg = DEFAULT_ALG 113*9c5db199SXin Li self.error_behavior = DEFAULT_ERROR_BEHAVIOR 114*9c5db199SXin Li self.blocks = DEFAULT_IMAGE_SIZE_IN_BLOCKS 115*9c5db199SXin Li self.file = None 116*9c5db199SXin Li self.has_fs = False 117*9c5db199SXin Li self.hash_file = None 118*9c5db199SXin Li self.table = None 119*9c5db199SXin Li self.target_name = DEFAULT_TARGET_NAME 120*9c5db199SXin Li 121*9c5db199SXin Li self.__initialized = False 122*9c5db199SXin Li 123*9c5db199SXin Li def __init__(self): 124*9c5db199SXin Li """Sets up the defaults for the object and then calls reset() 125*9c5db199SXin Li """ 126*9c5db199SXin Li self.reset() 127*9c5db199SXin Li 128*9c5db199SXin Li def __del__(self): 129*9c5db199SXin Li # Release any and all system resources. 130*9c5db199SXin Li self.reset() 131*9c5db199SXin Li 132*9c5db199SXin Li def _create_image(self): 133*9c5db199SXin Li """Creates a placeholder file.""" 134*9c5db199SXin Li # TODO(wad) replace with python 135*9c5db199SXin Li utils.system_output(self.dd_cmd % (self.file, self.blocks)) 136*9c5db199SXin Li 137*9c5db199SXin Li def _create_fs(self, copy_files): 138*9c5db199SXin Li """sets up ext3 on the image""" 139*9c5db199SXin Li self.has_fs = True 140*9c5db199SXin Li system(self.mkfs_cmd % self.file) 141*9c5db199SXin Li 142*9c5db199SXin Li def _hash_image(self): 143*9c5db199SXin Li """runs verity over the image and saves the device mapper table""" 144*9c5db199SXin Li self.table = utils.system_output(self.verity_cmd % (self.alg, 145*9c5db199SXin Li self.file, 146*9c5db199SXin Li self.blocks, 147*9c5db199SXin Li self.hash_file)) 148*9c5db199SXin Li # The verity tool doesn't include a templated error value. 149*9c5db199SXin Li # For now, we add one. 150*9c5db199SXin Li self.table += " error_behavior=ERROR_BEHAVIOR" 151*9c5db199SXin Li logging.info("table is %s", self.table) 152*9c5db199SXin Li 153*9c5db199SXin Li def _append_hash(self): 154*9c5db199SXin Li f = open(self.file, 'ab') 155*9c5db199SXin Li f.write(utils.read_file(self.hash_file)) 156*9c5db199SXin Li f.close() 157*9c5db199SXin Li 158*9c5db199SXin Li def _setup_loop(self): 159*9c5db199SXin Li # Setup a loop device 160*9c5db199SXin Li self.loop = utils.system_output('losetup -f --show %s' % (self.file)) 161*9c5db199SXin Li 162*9c5db199SXin Li def _setup_target(self): 163*9c5db199SXin Li # Update the table with the loop dev 164*9c5db199SXin Li self.table = self.table.replace('HASH_DEV', self.loop) 165*9c5db199SXin Li self.table = self.table.replace('ROOT_DEV', self.loop) 166*9c5db199SXin Li self.table = self.table.replace('ERROR_BEHAVIOR', self.error_behavior) 167*9c5db199SXin Li 168*9c5db199SXin Li system(self.dmsetup_cmd % (self.target_name, self.table)) 169*9c5db199SXin Li self.device = "/dev/mapper/autotest_%s" % self.target_name 170*9c5db199SXin Li 171*9c5db199SXin Li def initialize(self, 172*9c5db199SXin Li tmpdir, 173*9c5db199SXin Li target_name, 174*9c5db199SXin Li alg=DEFAULT_ALG, 175*9c5db199SXin Li size_in_blocks=DEFAULT_IMAGE_SIZE_IN_BLOCKS, 176*9c5db199SXin Li error_behavior=DEFAULT_ERROR_BEHAVIOR): 177*9c5db199SXin Li """Performs any required system-level initialization before use. 178*9c5db199SXin Li """ 179*9c5db199SXin Li try: 180*9c5db199SXin Li os_dep.commands('losetup', 'mkfs.ext3', 'dmsetup', 'verity', 'dd', 181*9c5db199SXin Li 'dumpe2fs') 182*9c5db199SXin Li except ValueError as e: 183*9c5db199SXin Li logging.error('verity_image cannot be used without: %s', e) 184*9c5db199SXin Li return False 185*9c5db199SXin Li 186*9c5db199SXin Li # Used for the mapper device name and the tmpfile names. 187*9c5db199SXin Li self.target_name = target_name 188*9c5db199SXin Li 189*9c5db199SXin Li # Reserve some files to use. 190*9c5db199SXin Li self.file = os.tempnam(tmpdir, '%s.img.' % self.target_name) 191*9c5db199SXin Li self.hash_file = os.tempnam(tmpdir, '%s.hash.' % self.target_name) 192*9c5db199SXin Li 193*9c5db199SXin Li # Set up the configurable bits. 194*9c5db199SXin Li self.alg = alg 195*9c5db199SXin Li self.error_behavior = error_behavior 196*9c5db199SXin Li self.blocks = size_in_blocks 197*9c5db199SXin Li 198*9c5db199SXin Li self.__initialized = True 199*9c5db199SXin Li return True 200*9c5db199SXin Li 201*9c5db199SXin Li def create_backing_image(self, size_in_blocks, with_fs=True, 202*9c5db199SXin Li copy_files=None): 203*9c5db199SXin Li """Creates an image file of the given number of blocks and if specified 204*9c5db199SXin Li will create a filesystem and copy any files in a copy_files list to 205*9c5db199SXin Li the fs. 206*9c5db199SXin Li """ 207*9c5db199SXin Li self.blocks = size_in_blocks 208*9c5db199SXin Li self._create_image() 209*9c5db199SXin Li 210*9c5db199SXin Li if with_fs is True: 211*9c5db199SXin Li self._create_fs(copy_files) 212*9c5db199SXin Li else: 213*9c5db199SXin Li if type(copy_files) is list and len(copy_files) != 0: 214*9c5db199SXin Li logging.warning("verity_image.initialize called with " \ 215*9c5db199SXin Li "files to copy but no fs") 216*9c5db199SXin Li 217*9c5db199SXin Li return self.file 218*9c5db199SXin Li 219*9c5db199SXin Li def prepare_backing_device(self): 220*9c5db199SXin Li """Hashes the backing image, appends it to the backing image, points 221*9c5db199SXin Li a loop device at it and returns the path to the loop.""" 222*9c5db199SXin Li self._hash_image() 223*9c5db199SXin Li self._append_hash() 224*9c5db199SXin Li self._setup_loop() 225*9c5db199SXin Li return self.loop 226*9c5db199SXin Li 227*9c5db199SXin Li def create_verity_device(self): 228*9c5db199SXin Li """Sets up the device mapper node and returns its path""" 229*9c5db199SXin Li self._setup_target() 230*9c5db199SXin Li return self.device 231*9c5db199SXin Li 232*9c5db199SXin Li def verifiable(self): 233*9c5db199SXin Li """Returns True if the dm-verity device does not throw any errors 234*9c5db199SXin Li when being walked completely or False if it does.""" 235*9c5db199SXin Li try: 236*9c5db199SXin Li if self.has_fs is True: 237*9c5db199SXin Li system('dumpe2fs %s' % self.device) 238*9c5db199SXin Li # TODO(wad) replace with mmap.mmap-based access 239*9c5db199SXin Li system('dd if=%s of=/dev/null bs=4096' % self.device) 240*9c5db199SXin Li return True 241*9c5db199SXin Li except error.CmdError as e: 242*9c5db199SXin Li return False 243*9c5db199SXin Li 244*9c5db199SXin Li 245*9c5db199SXin Liclass VerityImageTest(test.test): 246*9c5db199SXin Li """VerityImageTest provides a base class for verity_image tests 247*9c5db199SXin Li to be derived from. It sets up a verity_image object for use 248*9c5db199SXin Li and provides the function mod_and_test() to wrap simple test 249*9c5db199SXin Li cases for verity_images. 250*9c5db199SXin Li 251*9c5db199SXin Li See platform_DMVerityCorruption as an example usage. 252*9c5db199SXin Li """ 253*9c5db199SXin Li version = 1 254*9c5db199SXin Li image_blocks = DEFAULT_IMAGE_SIZE_IN_BLOCKS 255*9c5db199SXin Li 256*9c5db199SXin Li def initialize(self): 257*9c5db199SXin Li """Overrides test.initialize() to setup a verity_image""" 258*9c5db199SXin Li self.verity = verity_image() 259*9c5db199SXin Li 260*9c5db199SXin Li def mod_nothing(self, run_count, backing_path, block_size, block_count): 261*9c5db199SXin Li """Example callback for mod_and_test that does nothing.""" 262*9c5db199SXin Li pass 263*9c5db199SXin Li 264*9c5db199SXin Li def mod_and_test(self, modifier, count, expected): 265*9c5db199SXin Li """Takes in a callback |modifier| and runs it |count| times over 266*9c5db199SXin Li the verified image checking for |expected| out of verity.verifiable() 267*9c5db199SXin Li """ 268*9c5db199SXin Li tries = 0 269*9c5db199SXin Li while tries < count: 270*9c5db199SXin Li # Start fresh then modify each block in the image. 271*9c5db199SXin Li self.verity.reset() 272*9c5db199SXin Li self.verity.initialize(self.tmpdir, self.__class__.__name__) 273*9c5db199SXin Li backing_path = self.verity.create_backing_image(self.image_blocks) 274*9c5db199SXin Li loop_dev = self.verity.prepare_backing_device() 275*9c5db199SXin Li 276*9c5db199SXin Li modifier(tries, 277*9c5db199SXin Li backing_path, 278*9c5db199SXin Li BLOCK_SIZE, 279*9c5db199SXin Li self.image_blocks) 280*9c5db199SXin Li 281*9c5db199SXin Li mapped_dev = self.verity.create_verity_device() 282*9c5db199SXin Li 283*9c5db199SXin Li # Now check for failure. 284*9c5db199SXin Li if self.verity.verifiable() is not expected: 285*9c5db199SXin Li raise error.TestFail( 286*9c5db199SXin Li '%s: verity.verifiable() not as expected (%s)' % 287*9c5db199SXin Li (modifier.__name__, expected)) 288*9c5db199SXin Li tries += 1 289