Merge tag 'dm-pull-29oct19' of git://git.denx.de/u-boot-dm
[oweals/u-boot.git] / test / py / tests / test_fit.py
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
6 import os
7 import pytest
8 import struct
9 import u_boot_utils as util
10
11 # Define a base ITS which we can adjust using % and a dictionary
12 base_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
82 base_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.
99 base_script = '''
100 host load hostfs 0 %(fit_addr)x %(fit)s
101 fdt addr %(fit_addr)x
102 bootm start %(fit_addr)x
103 bootm loados
104 host save hostfs 0 %(kernel_addr)x %(kernel_out)s %(kernel_size)x
105 host save hostfs 0 %(fdt_addr)x %(fdt_out)s %(fdt_size)x
106 host save hostfs 0 %(ramdisk_addr)x %(ramdisk_out)s %(ramdisk_size)x
107 host save hostfs 0 %(loadables1_addr)x %(loadables1_out)s %(loadables1_size)x
108 host 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')
114 def 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()