]>
Commit | Line | Data |
---|---|---|
301e8038 SG |
1 | #!/usr/bin/python |
2 | # | |
3 | # Copyright (c) 2013, Google Inc. | |
4 | # | |
5 | # Sanity check of the FIT handling in U-Boot | |
6 | # | |
1a459660 | 7 | # SPDX-License-Identifier: GPL-2.0+ |
301e8038 SG |
8 | # |
9 | # To run this: | |
10 | # | |
11 | # make O=sandbox sandbox_config | |
12 | # make O=sandbox | |
13 | # ./test/image/test-fit.py -u sandbox/u-boot | |
14 | ||
15 | import doctest | |
16 | from optparse import OptionParser | |
17 | import os | |
18 | import shutil | |
19 | import struct | |
20 | import sys | |
21 | import tempfile | |
22 | ||
cc447728 SG |
23 | # Enable printing of all U-Boot output |
24 | DEBUG = True | |
25 | ||
301e8038 SG |
26 | # The 'command' library in patman is convenient for running commands |
27 | base_path = os.path.dirname(sys.argv[0]) | |
28 | patman = os.path.join(base_path, '../../tools/patman') | |
29 | sys.path.append(patman) | |
30 | ||
31 | import command | |
32 | ||
33 | # Define a base ITS which we can adjust using % and a dictionary | |
34 | base_its = ''' | |
35 | /dts-v1/; | |
36 | ||
37 | / { | |
38 | description = "Chrome OS kernel image with one or more FDT blobs"; | |
39 | #address-cells = <1>; | |
40 | ||
41 | images { | |
42 | kernel@1 { | |
43 | data = /incbin/("%(kernel)s"); | |
44 | type = "kernel"; | |
45 | arch = "sandbox"; | |
46 | os = "linux"; | |
47 | compression = "none"; | |
48 | load = <0x40000>; | |
49 | entry = <0x8>; | |
50 | }; | |
51 | fdt@1 { | |
52 | description = "snow"; | |
53 | data = /incbin/("u-boot.dtb"); | |
54 | type = "flat_dt"; | |
55 | arch = "sandbox"; | |
56 | %(fdt_load)s | |
57 | compression = "none"; | |
58 | signature@1 { | |
59 | algo = "sha1,rsa2048"; | |
60 | key-name-hint = "dev"; | |
61 | }; | |
62 | }; | |
63 | ramdisk@1 { | |
64 | description = "snow"; | |
65 | data = /incbin/("%(ramdisk)s"); | |
66 | type = "ramdisk"; | |
67 | arch = "sandbox"; | |
68 | os = "linux"; | |
69 | %(ramdisk_load)s | |
70 | compression = "none"; | |
71 | }; | |
72 | }; | |
73 | configurations { | |
74 | default = "conf@1"; | |
75 | conf@1 { | |
76 | kernel = "kernel@1"; | |
77 | fdt = "fdt@1"; | |
78 | %(ramdisk_config)s | |
79 | }; | |
80 | }; | |
81 | }; | |
82 | ''' | |
83 | ||
84 | # Define a base FDT - currently we don't use anything in this | |
85 | base_fdt = ''' | |
86 | /dts-v1/; | |
87 | ||
88 | / { | |
89 | model = "Sandbox Verified Boot Test"; | |
90 | compatible = "sandbox"; | |
91 | ||
92 | }; | |
93 | ''' | |
94 | ||
95 | # This is the U-Boot script that is run for each test. First load the fit, | |
96 | # then do the 'bootm' command, then save out memory from the places where | |
97 | # we expect 'bootm' to write things. Then quit. | |
98 | base_script = ''' | |
dfe6f4d6 | 99 | sb load hostfs 0 %(fit_addr)x %(fit)s |
301e8038 SG |
100 | fdt addr %(fit_addr)x |
101 | bootm start %(fit_addr)x | |
102 | bootm loados | |
b5493d17 SG |
103 | sb save hostfs 0 %(kernel_addr)x %(kernel_out)s %(kernel_size)x |
104 | sb save hostfs 0 %(fdt_addr)x %(fdt_out)s %(fdt_size)x | |
105 | sb save hostfs 0 %(ramdisk_addr)x %(ramdisk_out)s %(ramdisk_size)x | |
301e8038 SG |
106 | reset |
107 | ''' | |
108 | ||
cc447728 SG |
109 | def debug_stdout(stdout): |
110 | if DEBUG: | |
111 | print stdout | |
112 | ||
301e8038 SG |
113 | def make_fname(leaf): |
114 | """Make a temporary filename | |
115 | ||
116 | Args: | |
117 | leaf: Leaf name of file to create (within temporary directory) | |
118 | Return: | |
119 | Temporary filename | |
120 | """ | |
121 | global base_dir | |
122 | ||
123 | return os.path.join(base_dir, leaf) | |
124 | ||
125 | def filesize(fname): | |
126 | """Get the size of a file | |
127 | ||
128 | Args: | |
129 | fname: Filename to check | |
130 | Return: | |
131 | Size of file in bytes | |
132 | """ | |
133 | return os.stat(fname).st_size | |
134 | ||
135 | def read_file(fname): | |
136 | """Read the contents of a file | |
137 | ||
138 | Args: | |
139 | fname: Filename to read | |
140 | Returns: | |
141 | Contents of file as a string | |
142 | """ | |
143 | with open(fname, 'r') as fd: | |
144 | return fd.read() | |
145 | ||
146 | def make_dtb(): | |
147 | """Make a sample .dts file and compile it to a .dtb | |
148 | ||
149 | Returns: | |
150 | Filename of .dtb file created | |
151 | """ | |
152 | src = make_fname('u-boot.dts') | |
153 | dtb = make_fname('u-boot.dtb') | |
154 | with open(src, 'w') as fd: | |
155 | print >>fd, base_fdt | |
156 | command.Output('dtc', src, '-O', 'dtb', '-o', dtb) | |
157 | return dtb | |
158 | ||
159 | def make_its(params): | |
160 | """Make a sample .its file with parameters embedded | |
161 | ||
162 | Args: | |
163 | params: Dictionary containing parameters to embed in the %() strings | |
164 | Returns: | |
165 | Filename of .its file created | |
166 | """ | |
167 | its = make_fname('test.its') | |
168 | with open(its, 'w') as fd: | |
169 | print >>fd, base_its % params | |
170 | return its | |
171 | ||
172 | def make_fit(mkimage, params): | |
173 | """Make a sample .fit file ready for loading | |
174 | ||
175 | This creates a .its script with the selected parameters and uses mkimage to | |
176 | turn this into a .fit image. | |
177 | ||
178 | Args: | |
179 | mkimage: Filename of 'mkimage' utility | |
180 | params: Dictionary containing parameters to embed in the %() strings | |
181 | Return: | |
182 | Filename of .fit file created | |
183 | """ | |
184 | fit = make_fname('test.fit') | |
185 | its = make_its(params) | |
186 | command.Output(mkimage, '-f', its, fit) | |
187 | with open(make_fname('u-boot.dts'), 'w') as fd: | |
188 | print >>fd, base_fdt | |
189 | return fit | |
190 | ||
191 | def make_kernel(): | |
192 | """Make a sample kernel with test data | |
193 | ||
194 | Returns: | |
195 | Filename of kernel created | |
196 | """ | |
197 | fname = make_fname('test-kernel.bin') | |
198 | data = '' | |
199 | for i in range(100): | |
200 | data += 'this kernel %d is unlikely to boot\n' % i | |
201 | with open(fname, 'w') as fd: | |
202 | print >>fd, data | |
203 | return fname | |
204 | ||
205 | def make_ramdisk(): | |
206 | """Make a sample ramdisk with test data | |
207 | ||
208 | Returns: | |
209 | Filename of ramdisk created | |
210 | """ | |
211 | fname = make_fname('test-ramdisk.bin') | |
212 | data = '' | |
213 | for i in range(100): | |
214 | data += 'ramdisk %d was seldom used in the middle ages\n' % i | |
215 | with open(fname, 'w') as fd: | |
216 | print >>fd, data | |
217 | return fname | |
218 | ||
219 | def find_matching(text, match): | |
220 | """Find a match in a line of text, and return the unmatched line portion | |
221 | ||
222 | This is used to extract a part of a line from some text. The match string | |
223 | is used to locate the line - we use the first line that contains that | |
224 | match text. | |
225 | ||
226 | Once we find a match, we discard the match string itself from the line, | |
227 | and return what remains. | |
228 | ||
229 | TODO: If this function becomes more generally useful, we could change it | |
230 | to use regex and return groups. | |
231 | ||
232 | Args: | |
233 | text: Text to check (each line separated by \n) | |
234 | match: String to search for | |
235 | Return: | |
236 | String containing unmatched portion of line | |
237 | Exceptions: | |
238 | ValueError: If match is not found | |
239 | ||
240 | >>> find_matching('first line:10\\nsecond_line:20', 'first line:') | |
241 | '10' | |
242 | >>> find_matching('first line:10\\nsecond_line:20', 'second linex') | |
243 | Traceback (most recent call last): | |
244 | ... | |
245 | ValueError: Test aborted | |
246 | >>> find_matching('first line:10\\nsecond_line:20', 'second_line:') | |
247 | '20' | |
248 | """ | |
249 | for line in text.splitlines(): | |
250 | pos = line.find(match) | |
251 | if pos != -1: | |
252 | return line[:pos] + line[pos + len(match):] | |
253 | ||
254 | print "Expected '%s' but not found in output:" | |
255 | print text | |
256 | raise ValueError('Test aborted') | |
257 | ||
258 | def set_test(name): | |
259 | """Set the name of the current test and print a message | |
260 | ||
261 | Args: | |
262 | name: Name of test | |
263 | """ | |
264 | global test_name | |
265 | ||
266 | test_name = name | |
267 | print name | |
268 | ||
aec36cfd | 269 | def fail(msg, stdout): |
301e8038 SG |
270 | """Raise an error with a helpful failure message |
271 | ||
272 | Args: | |
273 | msg: Message to display | |
274 | """ | |
aec36cfd | 275 | print stdout |
301e8038 SG |
276 | raise ValueError("Test '%s' failed: %s" % (test_name, msg)) |
277 | ||
278 | def run_fit_test(mkimage, u_boot): | |
279 | """Basic sanity check of FIT loading in U-Boot | |
280 | ||
281 | TODO: Almost everything: | |
282 | - hash algorithms - invalid hash/contents should be detected | |
283 | - signature algorithms - invalid sig/contents should be detected | |
284 | - compression | |
285 | - checking that errors are detected like: | |
286 | - image overwriting | |
287 | - missing images | |
288 | - invalid configurations | |
289 | - incorrect os/arch/type fields | |
290 | - empty data | |
291 | - images too large/small | |
292 | - invalid FDT (e.g. putting a random binary in instead) | |
293 | - default configuration selection | |
294 | - bootm command line parameters should have desired effect | |
295 | - run code coverage to make sure we are testing all the code | |
296 | """ | |
297 | global test_name | |
298 | ||
299 | # Set up invariant files | |
300 | control_dtb = make_dtb() | |
301 | kernel = make_kernel() | |
302 | ramdisk = make_ramdisk() | |
303 | kernel_out = make_fname('kernel-out.bin') | |
304 | fdt_out = make_fname('fdt-out.dtb') | |
305 | ramdisk_out = make_fname('ramdisk-out.bin') | |
306 | ||
307 | # Set up basic parameters with default values | |
308 | params = { | |
309 | 'fit_addr' : 0x1000, | |
310 | ||
311 | 'kernel' : kernel, | |
312 | 'kernel_out' : kernel_out, | |
313 | 'kernel_addr' : 0x40000, | |
314 | 'kernel_size' : filesize(kernel), | |
315 | ||
316 | 'fdt_out' : fdt_out, | |
317 | 'fdt_addr' : 0x80000, | |
318 | 'fdt_size' : filesize(control_dtb), | |
319 | 'fdt_load' : '', | |
320 | ||
321 | 'ramdisk' : ramdisk, | |
322 | 'ramdisk_out' : ramdisk_out, | |
323 | 'ramdisk_addr' : 0xc0000, | |
324 | 'ramdisk_size' : filesize(ramdisk), | |
325 | 'ramdisk_load' : '', | |
326 | 'ramdisk_config' : '', | |
327 | } | |
328 | ||
329 | # Make a basic FIT and a script to load it | |
330 | fit = make_fit(mkimage, params) | |
331 | params['fit'] = fit | |
332 | cmd = base_script % params | |
333 | ||
334 | # First check that we can load a kernel | |
335 | # We could perhaps reduce duplication with some loss of readability | |
336 | set_test('Kernel load') | |
337 | stdout = command.Output(u_boot, '-d', control_dtb, '-c', cmd) | |
cc447728 | 338 | debug_stdout(stdout) |
301e8038 | 339 | if read_file(kernel) != read_file(kernel_out): |
aec36cfd | 340 | fail('Kernel not loaded', stdout) |
301e8038 | 341 | if read_file(control_dtb) == read_file(fdt_out): |
aec36cfd | 342 | fail('FDT loaded but should be ignored', stdout) |
301e8038 | 343 | if read_file(ramdisk) == read_file(ramdisk_out): |
aec36cfd | 344 | fail('Ramdisk loaded but should not be', stdout) |
301e8038 SG |
345 | |
346 | # Find out the offset in the FIT where U-Boot has found the FDT | |
347 | line = find_matching(stdout, 'Booting using the fdt blob at ') | |
348 | fit_offset = int(line, 16) - params['fit_addr'] | |
349 | fdt_magic = struct.pack('>L', 0xd00dfeed) | |
350 | data = read_file(fit) | |
351 | ||
352 | # Now find where it actually is in the FIT (skip the first word) | |
353 | real_fit_offset = data.find(fdt_magic, 4) | |
354 | if fit_offset != real_fit_offset: | |
355 | fail('U-Boot loaded FDT from offset %#x, FDT is actually at %#x' % | |
aec36cfd | 356 | (fit_offset, real_fit_offset), stdout) |
301e8038 SG |
357 | |
358 | # Now a kernel and an FDT | |
359 | set_test('Kernel + FDT load') | |
360 | params['fdt_load'] = 'load = <%#x>;' % params['fdt_addr'] | |
361 | fit = make_fit(mkimage, params) | |
362 | stdout = command.Output(u_boot, '-d', control_dtb, '-c', cmd) | |
cc447728 | 363 | debug_stdout(stdout) |
301e8038 | 364 | if read_file(kernel) != read_file(kernel_out): |
aec36cfd | 365 | fail('Kernel not loaded', stdout) |
301e8038 | 366 | if read_file(control_dtb) != read_file(fdt_out): |
aec36cfd | 367 | fail('FDT not loaded', stdout) |
301e8038 | 368 | if read_file(ramdisk) == read_file(ramdisk_out): |
aec36cfd | 369 | fail('Ramdisk loaded but should not be', stdout) |
301e8038 SG |
370 | |
371 | # Try a ramdisk | |
372 | set_test('Kernel + FDT + Ramdisk load') | |
373 | params['ramdisk_config'] = 'ramdisk = "ramdisk@1";' | |
374 | params['ramdisk_load'] = 'load = <%#x>;' % params['ramdisk_addr'] | |
375 | fit = make_fit(mkimage, params) | |
376 | stdout = command.Output(u_boot, '-d', control_dtb, '-c', cmd) | |
cc447728 | 377 | debug_stdout(stdout) |
301e8038 | 378 | if read_file(ramdisk) != read_file(ramdisk_out): |
aec36cfd | 379 | fail('Ramdisk not loaded', stdout) |
301e8038 SG |
380 | |
381 | def run_tests(): | |
382 | """Parse options, run the FIT tests and print the result""" | |
383 | global base_path, base_dir | |
384 | ||
385 | # Work in a temporary directory | |
386 | base_dir = tempfile.mkdtemp() | |
387 | parser = OptionParser() | |
388 | parser.add_option('-u', '--u-boot', | |
389 | default=os.path.join(base_path, 'u-boot'), | |
390 | help='Select U-Boot sandbox binary') | |
391 | parser.add_option('-k', '--keep', action='store_true', | |
392 | help="Don't delete temporary directory even when tests pass") | |
393 | parser.add_option('-t', '--selftest', action='store_true', | |
394 | help='Run internal self tests') | |
395 | (options, args) = parser.parse_args() | |
396 | ||
397 | # Find the path to U-Boot, and assume mkimage is in its tools/mkimage dir | |
398 | base_path = os.path.dirname(options.u_boot) | |
399 | mkimage = os.path.join(base_path, 'tools/mkimage') | |
400 | ||
401 | # There are a few doctests - handle these here | |
402 | if options.selftest: | |
403 | doctest.testmod() | |
404 | return | |
405 | ||
406 | title = 'FIT Tests' | |
407 | print title, '\n', '=' * len(title) | |
408 | ||
409 | run_fit_test(mkimage, options.u_boot) | |
410 | ||
411 | print '\nTests passed' | |
412 | print 'Caveat: this is only a sanity check - test coverage is poor' | |
413 | ||
414 | # Remove the tempoerary directory unless we are asked to keep it | |
415 | if options.keep: | |
416 | print "Output files are in '%s'" % base_dir | |
417 | else: | |
418 | shutil.rmtree(base_dir) | |
419 | ||
420 | run_tests() |