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