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