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