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