• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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