1# SPDX-License-Identifier: GPL-2.0+ 2# Copyright (c) 2013, Google Inc. 3# 4# Sanity check of the FIT handling in U-Boot 5 6import os 7import pytest 8import struct 9import utils 10import fit_util 11 12# Define a base ITS which we can adjust using % and a dictionary 13base_its = ''' 14/dts-v1/; 15 16/ { 17 description = "Chrome OS kernel image with one or more FDT blobs"; 18 #address-cells = <1>; 19 20 images { 21 kernel-1 { 22 data = /incbin/("%(kernel)s"); 23 type = "kernel"; 24 arch = "sandbox"; 25 os = "linux"; 26 compression = "%(compression)s"; 27 load = <0x40000>; 28 entry = <0x8>; 29 }; 30 kernel-2 { 31 data = /incbin/("%(loadables1)s"); 32 type = "kernel"; 33 arch = "sandbox"; 34 os = "linux"; 35 compression = "none"; 36 %(loadables1_load)s 37 entry = <0x0>; 38 }; 39 fdt-1 { 40 description = "snow"; 41 data = /incbin/("%(fdt)s"); 42 type = "flat_dt"; 43 arch = "sandbox"; 44 %(fdt_load)s 45 compression = "%(compression)s"; 46 signature-1 { 47 algo = "sha1,rsa2048"; 48 key-name-hint = "dev"; 49 }; 50 }; 51 ramdisk-1 { 52 description = "snow"; 53 data = /incbin/("%(ramdisk)s"); 54 type = "ramdisk"; 55 arch = "sandbox"; 56 os = "linux"; 57 %(ramdisk_load)s 58 compression = "%(compression)s"; 59 }; 60 ramdisk-2 { 61 description = "snow"; 62 data = /incbin/("%(loadables2)s"); 63 type = "ramdisk"; 64 arch = "sandbox"; 65 os = "linux"; 66 %(loadables2_load)s 67 compression = "none"; 68 }; 69 }; 70 configurations { 71 default = "conf-1"; 72 conf-1 { 73 kernel = "kernel-1"; 74 fdt = "fdt-1"; 75 %(ramdisk_config)s 76 %(loadables_config)s 77 }; 78 }; 79}; 80''' 81 82# Define a base FDT - currently we don't use anything in this 83base_fdt = ''' 84/dts-v1/; 85 86/ { 87 #address-cells = <1>; 88 #size-cells = <0>; 89 90 model = "Sandbox Verified Boot Test"; 91 compatible = "sandbox"; 92 93 binman { 94 }; 95 96 reset@0 { 97 compatible = "sandbox,reset"; 98 reg = <0>; 99 }; 100}; 101''' 102 103# This is the U-Boot script that is run for each test. First load the FIT, 104# then run the 'bootm' command, then save out memory from the places where 105# we expect 'bootm' to write things. Then quit. 106base_script = ''' 107host load hostfs 0 %(fit_addr)x %(fit)s 108fdt addr %(fit_addr)x 109bootm start %(fit_addr)x 110bootm loados 111host save hostfs 0 %(kernel_addr)x %(kernel_out)s %(kernel_size)x 112host save hostfs 0 %(fdt_addr)x %(fdt_out)s %(fdt_size)x 113host save hostfs 0 %(ramdisk_addr)x %(ramdisk_out)s %(ramdisk_size)x 114host save hostfs 0 %(loadables1_addr)x %(loadables1_out)s %(loadables1_size)x 115host save hostfs 0 %(loadables2_addr)x %(loadables2_out)s %(loadables2_size)x 116''' 117 118@pytest.mark.boardspec('sandbox') 119@pytest.mark.buildconfigspec('fit_signature') 120@pytest.mark.requiredtool('dtc') 121def test_fit(ubman): 122 def make_fname(leaf): 123 """Make a temporary filename 124 125 Args: 126 leaf: Leaf name of file to create (within temporary directory) 127 Return: 128 Temporary filename 129 """ 130 return os.path.join(ubman.config.build_dir, leaf) 131 132 def filesize(fname): 133 """Get the size of a file 134 135 Args: 136 fname: Filename to check 137 Return: 138 Size of file in bytes 139 """ 140 return os.stat(fname).st_size 141 142 def read_file(fname): 143 """Read the contents of a file 144 145 Args: 146 fname: Filename to read 147 Returns: 148 Contents of file as a string 149 """ 150 with open(fname, 'rb') as fd: 151 return fd.read() 152 153 def make_ramdisk(filename, text): 154 """Make a sample ramdisk with test data 155 156 Returns: 157 Filename of ramdisk created 158 """ 159 fname = make_fname(filename) 160 data = '' 161 for i in range(100): 162 data += '%s %d was seldom used in the middle ages\n' % (text, i) 163 with open(fname, 'w') as fd: 164 print(data, file=fd) 165 return fname 166 167 def make_compressed(filename): 168 utils.run_and_log(ubman, ['gzip', '-f', '-k', filename]) 169 return filename + '.gz' 170 171 def find_matching(text, match): 172 """Find a match in a line of text, and return the unmatched line portion 173 174 This is used to extract a part of a line from some text. The match string 175 is used to locate the line - we use the first line that contains that 176 match text. 177 178 Once we find a match, we discard the match string itself from the line, 179 and return what remains. 180 181 TODO: If this function becomes more generally useful, we could change it 182 to use regex and return groups. 183 184 Args: 185 text: Text to check (list of strings, one for each command issued) 186 match: String to search for 187 Return: 188 String containing unmatched portion of line 189 Exceptions: 190 ValueError: If match is not found 191 192 >>> find_matching(['first line:10', 'second_line:20'], 'first line:') 193 '10' 194 >>> find_matching(['first line:10', 'second_line:20'], 'second line') 195 Traceback (most recent call last): 196 ... 197 ValueError: Test aborted 198 >>> find_matching('first line:10\', 'second_line:20'], 'second_line:') 199 '20' 200 >>> find_matching('first line:10\', 'second_line:20\nthird_line:30'], 201 'third_line:') 202 '30' 203 """ 204 __tracebackhide__ = True 205 for line in '\n'.join(text).splitlines(): 206 pos = line.find(match) 207 if pos != -1: 208 return line[:pos] + line[pos + len(match):] 209 210 pytest.fail("Expected '%s' but not found in output") 211 212 def check_equal(expected_fname, actual_fname, failure_msg): 213 """Check that a file matches its expected contents 214 215 This is always used on out-buffers whose size is decided by the test 216 script anyway, which in some cases may be larger than what we're 217 actually looking for. So it's safe to truncate it to the size of the 218 expected data. 219 220 Args: 221 expected_fname: Filename containing expected contents 222 actual_fname: Filename containing actual contents 223 failure_msg: Message to print on failure 224 """ 225 expected_data = read_file(expected_fname) 226 actual_data = read_file(actual_fname) 227 if len(expected_data) < len(actual_data): 228 actual_data = actual_data[:len(expected_data)] 229 assert expected_data == actual_data, failure_msg 230 231 def check_not_equal(expected_fname, actual_fname, failure_msg): 232 """Check that a file does not match its expected contents 233 234 Args: 235 expected_fname: Filename containing expected contents 236 actual_fname: Filename containing actual contents 237 failure_msg: Message to print on failure 238 """ 239 expected_data = read_file(expected_fname) 240 actual_data = read_file(actual_fname) 241 assert expected_data != actual_data, failure_msg 242 243 def run_fit_test(mkimage): 244 """Basic sanity check of FIT loading in U-Boot 245 246 TODO: Almost everything: 247 - hash algorithms - invalid hash/contents should be detected 248 - signature algorithms - invalid sig/contents should be detected 249 - compression 250 - checking that errors are detected like: 251 - image overwriting 252 - missing images 253 - invalid configurations 254 - incorrect os/arch/type fields 255 - empty data 256 - images too large/small 257 - invalid FDT (e.g. putting a random binary in instead) 258 - default configuration selection 259 - bootm command line parameters should have desired effect 260 - run code coverage to make sure we are testing all the code 261 """ 262 # Set up invariant files 263 control_dtb = fit_util.make_dtb(ubman, base_fdt, 'u-boot') 264 kernel = fit_util.make_kernel(ubman, 'test-kernel.bin', 'kernel') 265 ramdisk = make_ramdisk('test-ramdisk.bin', 'ramdisk') 266 loadables1 = fit_util.make_kernel(ubman, 'test-loadables1.bin', 'lenrek') 267 loadables2 = make_ramdisk('test-loadables2.bin', 'ksidmar') 268 kernel_out = make_fname('kernel-out.bin') 269 fdt = make_fname('u-boot.dtb') 270 fdt_out = make_fname('fdt-out.dtb') 271 ramdisk_out = make_fname('ramdisk-out.bin') 272 loadables1_out = make_fname('loadables1-out.bin') 273 loadables2_out = make_fname('loadables2-out.bin') 274 275 # Set up basic parameters with default values 276 params = { 277 'fit_addr' : 0x1000, 278 279 'kernel' : kernel, 280 'kernel_out' : kernel_out, 281 'kernel_addr' : 0x40000, 282 'kernel_size' : filesize(kernel), 283 284 'fdt' : fdt, 285 'fdt_out' : fdt_out, 286 'fdt_addr' : 0x80000, 287 'fdt_size' : filesize(control_dtb), 288 'fdt_load' : '', 289 290 'ramdisk' : ramdisk, 291 'ramdisk_out' : ramdisk_out, 292 'ramdisk_addr' : 0xc0000, 293 'ramdisk_size' : filesize(ramdisk), 294 'ramdisk_load' : '', 295 'ramdisk_config' : '', 296 297 'loadables1' : loadables1, 298 'loadables1_out' : loadables1_out, 299 'loadables1_addr' : 0x100000, 300 'loadables1_size' : filesize(loadables1), 301 'loadables1_load' : '', 302 303 'loadables2' : loadables2, 304 'loadables2_out' : loadables2_out, 305 'loadables2_addr' : 0x140000, 306 'loadables2_size' : filesize(loadables2), 307 'loadables2_load' : '', 308 309 'loadables_config' : '', 310 'compression' : 'none', 311 } 312 313 # Make a basic FIT and a script to load it 314 fit = fit_util.make_fit(ubman, mkimage, base_its, params) 315 params['fit'] = fit 316 cmd = base_script % params 317 318 # First check that we can load a kernel 319 # We could perhaps reduce duplication with some loss of readability 320 ubman.config.dtb = control_dtb 321 ubman.restart_uboot() 322 with ubman.log.section('Kernel load'): 323 output = ubman.run_command_list(cmd.splitlines()) 324 check_equal(kernel, kernel_out, 'Kernel not loaded') 325 check_not_equal(control_dtb, fdt_out, 326 'FDT loaded but should be ignored') 327 check_not_equal(ramdisk, ramdisk_out, 328 'Ramdisk loaded but should not be') 329 330 # Find out the offset in the FIT where U-Boot has found the FDT 331 line = find_matching(output, 'Booting using the fdt blob at ') 332 fit_offset = int(line, 16) - params['fit_addr'] 333 fdt_magic = struct.pack('>L', 0xd00dfeed) 334 data = read_file(fit) 335 336 # Now find where it actually is in the FIT (skip the first word) 337 real_fit_offset = data.find(fdt_magic, 4) 338 assert fit_offset == real_fit_offset, ( 339 'U-Boot loaded FDT from offset %#x, FDT is actually at %#x' % 340 (fit_offset, real_fit_offset)) 341 342 # Check if bootargs strings substitution works 343 output = ubman.run_command_list([ 344 'env set bootargs \\\"\'my_boot_var=${foo}\'\\\"', 345 'env set foo bar', 346 'bootm prep', 347 'env print bootargs']) 348 assert 'bootargs="my_boot_var=bar"' in output, "Bootargs strings not substituted" 349 350 # Now a kernel and an FDT 351 with ubman.log.section('Kernel + FDT load'): 352 params['fdt_load'] = 'load = <%#x>;' % params['fdt_addr'] 353 fit = fit_util.make_fit(ubman, mkimage, base_its, params) 354 ubman.restart_uboot() 355 output = ubman.run_command_list(cmd.splitlines()) 356 check_equal(kernel, kernel_out, 'Kernel not loaded') 357 check_equal(control_dtb, fdt_out, 'FDT not loaded') 358 check_not_equal(ramdisk, ramdisk_out, 359 'Ramdisk loaded but should not be') 360 361 # Try a ramdisk 362 with ubman.log.section('Kernel + FDT + Ramdisk load'): 363 params['ramdisk_config'] = 'ramdisk = "ramdisk-1";' 364 params['ramdisk_load'] = 'load = <%#x>;' % params['ramdisk_addr'] 365 fit = fit_util.make_fit(ubman, mkimage, base_its, params) 366 ubman.restart_uboot() 367 output = ubman.run_command_list(cmd.splitlines()) 368 check_equal(ramdisk, ramdisk_out, 'Ramdisk not loaded') 369 370 # Configuration with some Loadables 371 with ubman.log.section('Kernel + FDT + Ramdisk load + Loadables'): 372 params['loadables_config'] = 'loadables = "kernel-2", "ramdisk-2";' 373 params['loadables1_load'] = ('load = <%#x>;' % 374 params['loadables1_addr']) 375 params['loadables2_load'] = ('load = <%#x>;' % 376 params['loadables2_addr']) 377 fit = fit_util.make_fit(ubman, mkimage, base_its, params) 378 ubman.restart_uboot() 379 output = ubman.run_command_list(cmd.splitlines()) 380 check_equal(loadables1, loadables1_out, 381 'Loadables1 (kernel) not loaded') 382 check_equal(loadables2, loadables2_out, 383 'Loadables2 (ramdisk) not loaded') 384 385 # Kernel, FDT and Ramdisk all compressed 386 with ubman.log.section('(Kernel + FDT + Ramdisk) compressed'): 387 params['compression'] = 'gzip' 388 params['kernel'] = make_compressed(kernel) 389 params['fdt'] = make_compressed(fdt) 390 params['ramdisk'] = make_compressed(ramdisk) 391 fit = fit_util.make_fit(ubman, mkimage, base_its, params) 392 ubman.restart_uboot() 393 output = ubman.run_command_list(cmd.splitlines()) 394 check_equal(kernel, kernel_out, 'Kernel not loaded') 395 check_equal(control_dtb, fdt_out, 'FDT not loaded') 396 check_not_equal(ramdisk, ramdisk_out, 'Ramdisk got decompressed?') 397 check_equal(ramdisk + '.gz', ramdisk_out, 'Ramdist not loaded') 398 399 400 # We need to use our own device tree file. Remember to restore it 401 # afterwards. 402 old_dtb = ubman.config.dtb 403 try: 404 mkimage = ubman.config.build_dir + '/tools/mkimage' 405 run_fit_test(mkimage) 406 finally: 407 # Go back to the original U-Boot with the correct dtb. 408 ubman.config.dtb = old_dtb 409 ubman.restart_uboot() 410