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 5 import logging, mmap, os, time 6 7 import common 8 from autotest_lib.client.bin import os_dep, test 9 from 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. 14 ERROR_BEHAVIOR_ERROR = 'eio' 15 ERROR_BEHAVIOR_REBOOT = 'panic' 16 ERROR_BEHAVIOR_IGNORE = 'none' 17 ERROR_BEHAVIOR_NOTIFIER = 'notify' # for platform specific behavior. 18 19 # Default configuration for verity_image 20 DEFAULT_TARGET_NAME = 'verity_image' 21 DEFAULT_ALG = 'sha1' 22 DEFAULT_IMAGE_SIZE_IN_BLOCKS = 100 23 DEFAULT_ERROR_BEHAVIOR = ERROR_BEHAVIOR_ERROR 24 # TODO(wad) make this configurable when dm-verity doesn't hard-code 4096. 25 BLOCK_SIZE = 4096 26 27 def 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 38 class 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 ignore_status=True) 74 lsblk = utils.system_output('lsblk %s' % (device), 75 retain_output=True, 76 ignore_status=True) 77 time.sleep(1) 78 if utils.system(cmd, ignore_status=True) == 0: 79 return 80 raise error.TestFail('"%s" failed: %s\n%s' % (cmd, fuser, lsblk)) 81 82 def reset(self): 83 """Idempotent call which will free any claimed system resources""" 84 # Pre-initialize these values to None 85 for attr in ['mountpoint', 'device', 'loop', 'file', 'hash_file']: 86 if not hasattr(self, attr): 87 setattr(self, attr, None) 88 logging.info("verity_image is being reset") 89 90 if self.mountpoint is not None: 91 system('umount %s' % self.mountpoint) 92 self.mountpoint = None 93 94 if self.device is not None: 95 self._device_release('dmsetup remove %s' % (self.device), 96 self.device) 97 self.device = None 98 99 if self.loop is not None: 100 self._device_release('losetup -d %s' % (self.loop), self.loop) 101 self.loop = None 102 103 if self.file is not None: 104 os.remove(self.file) 105 self.file = None 106 107 if self.hash_file is not None: 108 os.remove(self.hash_file) 109 self.hash_file = None 110 111 self.alg = DEFAULT_ALG 112 self.error_behavior = DEFAULT_ERROR_BEHAVIOR 113 self.blocks = DEFAULT_IMAGE_SIZE_IN_BLOCKS 114 self.file = None 115 self.has_fs = False 116 self.hash_file = None 117 self.table = None 118 self.target_name = DEFAULT_TARGET_NAME 119 120 self.__initialized = False 121 122 def __init__(self): 123 """Sets up the defaults for the object and then calls reset() 124 """ 125 self.reset() 126 127 def __del__(self): 128 # Release any and all system resources. 129 self.reset() 130 131 def _create_image(self): 132 """Creates a dummy file.""" 133 # TODO(wad) replace with python 134 utils.system_output(self.dd_cmd % (self.file, self.blocks)) 135 136 def _create_fs(self, copy_files): 137 """sets up ext3 on the image""" 138 self.has_fs = True 139 system(self.mkfs_cmd % self.file) 140 if type(copy_files) is list: 141 for file in copy_files: 142 pass # TODO(wad) 143 144 def _hash_image(self): 145 """runs verity over the image and saves the device mapper table""" 146 self.table = utils.system_output(self.verity_cmd % (self.alg, 147 self.file, 148 self.blocks, 149 self.hash_file)) 150 # The verity tool doesn't include a templated error value. 151 # For now, we add one. 152 self.table += " error_behavior=ERROR_BEHAVIOR" 153 logging.info("table is %s" % self.table) 154 155 def _append_hash(self): 156 f = open(self.file, 'ab') 157 f.write(utils.read_file(self.hash_file)) 158 f.close() 159 160 def _setup_loop(self): 161 # Setup a loop device 162 self.loop = utils.system_output('losetup -f --show %s' % (self.file)) 163 164 def _setup_target(self): 165 # Update the table with the loop dev 166 self.table = self.table.replace('HASH_DEV', self.loop) 167 self.table = self.table.replace('ROOT_DEV', self.loop) 168 self.table = self.table.replace('ERROR_BEHAVIOR', self.error_behavior) 169 170 system(self.dmsetup_cmd % (self.target_name, self.table)) 171 self.device = "/dev/mapper/autotest_%s" % self.target_name 172 173 def initialize(self, 174 tmpdir, 175 target_name, 176 alg=DEFAULT_ALG, 177 size_in_blocks=DEFAULT_IMAGE_SIZE_IN_BLOCKS, 178 error_behavior=DEFAULT_ERROR_BEHAVIOR): 179 """Performs any required system-level initialization before use. 180 """ 181 try: 182 os_dep.commands('losetup', 'mkfs.ext3', 'dmsetup', 'verity', 'dd', 183 'dumpe2fs') 184 except ValueError, e: 185 logging.error('verity_image cannot be used without: %s' % e) 186 return False 187 188 # Used for the mapper device name and the tmpfile names. 189 self.target_name = target_name 190 191 # Reserve some files to use. 192 self.file = os.tempnam(tmpdir, '%s.img.' % self.target_name) 193 self.hash_file = os.tempnam(tmpdir, '%s.hash.' % self.target_name) 194 195 # Set up the configurable bits. 196 self.alg = alg 197 self.error_behavior = error_behavior 198 self.blocks = size_in_blocks 199 200 self.__initialized = True 201 return True 202 203 def create_backing_image(self, size_in_blocks, with_fs=True, 204 copy_files=None): 205 """Creates an image file of the given number of blocks and if specified 206 will create a filesystem and copy any files in a copy_files list to 207 the fs. 208 """ 209 self.blocks = size_in_blocks 210 self._create_image() 211 212 if with_fs is True: 213 self._create_fs(copy_files) 214 else: 215 if type(copy_files) is list and len(copy_files) != 0: 216 logging.warning("verity_image.initialize called with " \ 217 "files to copy but no fs") 218 219 return self.file 220 221 def prepare_backing_device(self): 222 """Hashes the backing image, appends it to the backing image, points 223 a loop device at it and returns the path to the loop.""" 224 self._hash_image() 225 self._append_hash() 226 self._setup_loop() 227 return self.loop 228 229 def create_verity_device(self): 230 """Sets up the device mapper node and returns its path""" 231 self._setup_target() 232 return self.device 233 234 def verifiable(self): 235 """Returns True if the dm-verity device does not throw any errors 236 when being walked completely or False if it does.""" 237 try: 238 if self.has_fs is True: 239 system('dumpe2fs %s' % self.device) 240 # TODO(wad) replace with mmap.mmap-based access 241 system('dd if=%s of=/dev/null bs=4096' % self.device) 242 return True 243 except error.CmdError, e: 244 return False 245 246 247 class VerityImageTest(test.test): 248 """VerityImageTest provides a base class for verity_image tests 249 to be derived from. It sets up a verity_image object for use 250 and provides the function mod_and_test() to wrap simple test 251 cases for verity_images. 252 253 See platform_DMVerityCorruption as an example usage. 254 """ 255 version = 1 256 image_blocks = DEFAULT_IMAGE_SIZE_IN_BLOCKS 257 258 def initialize(self): 259 """Overrides test.initialize() to setup a verity_image""" 260 self.verity = verity_image() 261 262 # Example callback for mod_and_test that does nothing 263 def mod_nothing(self, run_count, backing_path, block_size, block_count): 264 pass 265 266 def mod_and_test(self, modifier, count, expected): 267 """Takes in a callback |modifier| and runs it |count| times over 268 the verified image checking for |expected| out of verity.verifiable() 269 """ 270 tries = 0 271 while tries < count: 272 # Start fresh then modify each block in the image. 273 self.verity.reset() 274 self.verity.initialize(self.tmpdir, self.__class__.__name__) 275 backing_path = self.verity.create_backing_image(self.image_blocks) 276 loop_dev = self.verity.prepare_backing_device() 277 278 modifier(tries, 279 backing_path, 280 BLOCK_SIZE, 281 self.image_blocks) 282 283 mapped_dev = self.verity.create_verity_device() 284 285 # Now check for failure. 286 if self.verity.verifiable() is not expected: 287 raise error.TestFail( 288 '%s: verity.verifiable() not as expected (%s)' % 289 (modifier.__name__, expected)) 290 tries += 1 291