1# Lint as: python2, python3 2# Copyright (c) 2012 The Chromium OS Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6import logging, mmap, os, time 7 8import common 9from autotest_lib.client.bin import os_dep, test 10from autotest_lib.client.common_lib import error, logging_manager, utils 11 12""" a wrapper for using verity/dm-verity with a test backing store """ 13 14# enum for the 3 possible values of the module parameter. 15ERROR_BEHAVIOR_ERROR = 'eio' 16ERROR_BEHAVIOR_REBOOT = 'panic' 17ERROR_BEHAVIOR_IGNORE = 'none' 18ERROR_BEHAVIOR_NOTIFIER = 'notify' # for platform specific behavior. 19 20# Default configuration for verity_image 21DEFAULT_TARGET_NAME = 'verity_image' 22DEFAULT_ALG = 'sha256' 23DEFAULT_IMAGE_SIZE_IN_BLOCKS = 100 24DEFAULT_ERROR_BEHAVIOR = ERROR_BEHAVIOR_ERROR 25# TODO(wad) make this configurable when dm-verity doesn't hard-code 4096. 26BLOCK_SIZE = 4096 27 28def system(command, timeout=None): 29 """Delegate to utils.system to run |command|, logs stderr only on fail. 30 31 Runs |command|, captures stdout and stderr. Logs stdout to the DEBUG 32 log no matter what, logs stderr only if the command actually fails. 33 Will time the command out after |timeout|. 34 """ 35 utils.run(command, timeout=timeout, ignore_status=False, 36 stdout_tee=utils.TEE_TO_LOGS, stderr_tee=utils.TEE_TO_LOGS, 37 stderr_is_expected=True) 38 39class verity_image(object): 40 """ a helper for creating dm-verity targets for testing. 41 42 To use, 43 vi = verity_image() 44 vi.initialize(self.tmpdir, "dmveritytesta") 45 # Create a 409600 byte image with /bin/ls on it 46 # The size in bytes is returned. 47 backing_path = vi.create_backing_image(100, copy_files=['/bin/ls']) 48 # Performs hashing of the backing_path and sets up a device. 49 loop_dev = vi.prepare_backing_device() 50 # Sets up the mapped device and returns the path: 51 # E.g., /dev/mapper/autotest_dmveritytesta 52 dev = vi.create_verity_device() 53 # Access the mapped device using the returned string. 54 55 TODO(wad) add direct verified and backing store access functions 56 to make writing modifiers easier (e.g., mmap). 57 """ 58 # Define the command template constants. 59 verity_cmd = \ 60 'verity mode=create alg=%s payload=%s payload_blocks=%d hashtree=%s' 61 dd_cmd = 'dd if=/dev/zero of=%s bs=4096 count=0 seek=%d' 62 mkfs_cmd = 'mkfs.ext3 -b 4096 -F %s' 63 dmsetup_cmd = "dmsetup -r create autotest_%s --table '%s'" 64 65 def _device_release(self, cmd, device): 66 if utils.system(cmd, ignore_status=True) == 0: 67 return 68 logging.warning("Could not release %s. Retrying...", device) 69 # Other things (like cros-disks) may have the device open briefly, 70 # so if we initially fail, try again and attempt to gather details 71 # on who else is using the device. 72 fuser = utils.system_output('fuser -v %s' % (device), 73 retain_output=True, 74 ignore_status=True) 75 lsblk = utils.system_output('lsblk %s' % (device), 76 retain_output=True, 77 ignore_status=True) 78 time.sleep(1) 79 if utils.system(cmd, ignore_status=True) == 0: 80 return 81 raise error.TestFail('"%s" failed: %s\n%s' % (cmd, fuser, lsblk)) 82 83 def reset(self): 84 """Idempotent call which will free any claimed system resources""" 85 # Pre-initialize these values to None 86 for attr in ['mountpoint', 'device', 'loop', 'file', 'hash_file']: 87 if not hasattr(self, attr): 88 setattr(self, attr, None) 89 logging.info("verity_image is being reset") 90 91 if self.mountpoint is not None: 92 system('umount %s' % self.mountpoint) 93 self.mountpoint = None 94 95 if self.device is not None: 96 self._device_release('dmsetup remove %s' % (self.device), 97 self.device) 98 self.device = None 99 100 if self.loop is not None: 101 self._device_release('losetup -d %s' % (self.loop), self.loop) 102 self.loop = None 103 104 if self.file is not None: 105 os.remove(self.file) 106 self.file = None 107 108 if self.hash_file is not None: 109 os.remove(self.hash_file) 110 self.hash_file = None 111 112 self.alg = DEFAULT_ALG 113 self.error_behavior = DEFAULT_ERROR_BEHAVIOR 114 self.blocks = DEFAULT_IMAGE_SIZE_IN_BLOCKS 115 self.file = None 116 self.has_fs = False 117 self.hash_file = None 118 self.table = None 119 self.target_name = DEFAULT_TARGET_NAME 120 121 self.__initialized = False 122 123 def __init__(self): 124 """Sets up the defaults for the object and then calls reset() 125 """ 126 self.reset() 127 128 def __del__(self): 129 # Release any and all system resources. 130 self.reset() 131 132 def _create_image(self): 133 """Creates a dummy file.""" 134 # TODO(wad) replace with python 135 utils.system_output(self.dd_cmd % (self.file, self.blocks)) 136 137 def _create_fs(self, copy_files): 138 """sets up ext3 on the image""" 139 self.has_fs = True 140 system(self.mkfs_cmd % self.file) 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 as 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 as 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 def mod_nothing(self, run_count, backing_path, block_size, block_count): 261 """Example callback for mod_and_test that does nothing.""" 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