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