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