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 5import logging, mmap, os, time 6 7import common 8from autotest_lib.client.bin import os_dep, test 9from 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. 14ERROR_BEHAVIOR_ERROR = 'eio' 15ERROR_BEHAVIOR_REBOOT = 'panic' 16ERROR_BEHAVIOR_IGNORE = 'none' 17ERROR_BEHAVIOR_NOTIFIER = 'notify' # for platform specific behavior. 18 19# Default configuration for verity_image 20DEFAULT_TARGET_NAME = 'verity_image' 21DEFAULT_ALG = 'sha1' 22DEFAULT_IMAGE_SIZE_IN_BLOCKS = 100 23DEFAULT_ERROR_BEHAVIOR = ERROR_BEHAVIOR_ERROR 24# TODO(wad) make this configurable when dm-verity doesn't hard-code 4096. 25BLOCK_SIZE = 4096 26 27def 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 38class 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 245class 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