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 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(u_boot_console): 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(cons.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 util.run_and_log(cons, ['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(cons, base_fdt, 'u-boot') 264 kernel = fit_util.make_kernel(cons, 'test-kernel.bin', 'kernel') 265 ramdisk = make_ramdisk('test-ramdisk.bin', 'ramdisk') 266 loadables1 = fit_util.make_kernel(cons, '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(cons, 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 cons.config.dtb = control_dtb 321 cons.restart_uboot() 322 with cons.log.section('Kernel load'): 323 output = cons.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 # Now a kernel and an FDT 343 with cons.log.section('Kernel + FDT load'): 344 params['fdt_load'] = 'load = <%#x>;' % params['fdt_addr'] 345 fit = fit_util.make_fit(cons, mkimage, base_its, params) 346 cons.restart_uboot() 347 output = cons.run_command_list(cmd.splitlines()) 348 check_equal(kernel, kernel_out, 'Kernel not loaded') 349 check_equal(control_dtb, fdt_out, 'FDT not loaded') 350 check_not_equal(ramdisk, ramdisk_out, 351 'Ramdisk loaded but should not be') 352 353 # Try a ramdisk 354 with cons.log.section('Kernel + FDT + Ramdisk load'): 355 params['ramdisk_config'] = 'ramdisk = "ramdisk-1";' 356 params['ramdisk_load'] = 'load = <%#x>;' % params['ramdisk_addr'] 357 fit = fit_util.make_fit(cons, mkimage, base_its, params) 358 cons.restart_uboot() 359 output = cons.run_command_list(cmd.splitlines()) 360 check_equal(ramdisk, ramdisk_out, 'Ramdisk not loaded') 361 362 # Configuration with some Loadables 363 with cons.log.section('Kernel + FDT + Ramdisk load + Loadables'): 364 params['loadables_config'] = 'loadables = "kernel-2", "ramdisk-2";' 365 params['loadables1_load'] = ('load = <%#x>;' % 366 params['loadables1_addr']) 367 params['loadables2_load'] = ('load = <%#x>;' % 368 params['loadables2_addr']) 369 fit = fit_util.make_fit(cons, mkimage, base_its, params) 370 cons.restart_uboot() 371 output = cons.run_command_list(cmd.splitlines()) 372 check_equal(loadables1, loadables1_out, 373 'Loadables1 (kernel) not loaded') 374 check_equal(loadables2, loadables2_out, 375 'Loadables2 (ramdisk) not loaded') 376 377 # Kernel, FDT and Ramdisk all compressed 378 with cons.log.section('(Kernel + FDT + Ramdisk) compressed'): 379 params['compression'] = 'gzip' 380 params['kernel'] = make_compressed(kernel) 381 params['fdt'] = make_compressed(fdt) 382 params['ramdisk'] = make_compressed(ramdisk) 383 fit = fit_util.make_fit(cons, mkimage, base_its, params) 384 cons.restart_uboot() 385 output = cons.run_command_list(cmd.splitlines()) 386 check_equal(kernel, kernel_out, 'Kernel not loaded') 387 check_equal(control_dtb, fdt_out, 'FDT not loaded') 388 check_not_equal(ramdisk, ramdisk_out, 'Ramdisk got decompressed?') 389 check_equal(ramdisk + '.gz', ramdisk_out, 'Ramdist not loaded') 390 391 392 cons = u_boot_console 393 try: 394 # We need to use our own device tree file. Remember to restore it 395 # afterwards. 396 old_dtb = cons.config.dtb 397 mkimage = cons.config.build_dir + '/tools/mkimage' 398 run_fit_test(mkimage) 399 finally: 400 # Go back to the original U-Boot with the correct dtb. 401 cons.config.dtb = old_dtb 402 cons.restart_uboot() 403