• Home
  • History
  • Annotate
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 # Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
4 
5 """A module to support automatic firmware update.
6 
7 See FirmwareUpdater object below.
8 """
9 
10 import os
11 import re
12 
13 from autotest_lib.client.common_lib.cros import chip_utils
14 from autotest_lib.client.cros.faft.utils import (common,
15                                                  flashrom_handler,
16                                                  saft_flashrom_util,
17                                                  shell_wrapper)
18 
19 
20 class FirmwareUpdaterError(Exception):
21     """Error in the FirmwareUpdater module."""
22 
23 
24 class FirmwareUpdater(object):
25     """An object to support firmware update.
26 
27     This object will create a temporary directory in /var/tmp/faft/autest with
28     two subdirectory keys/ and work/. You can modify the keys in keys/
29     directory. If you want to provide a given shellball to do firmware update,
30     put shellball under /var/tmp/faft/autest with name chromeos-firmwareupdate.
31     """
32 
33     DAEMON = 'update-engine'
34     CBFSTOOL = 'cbfstool'
35     HEXDUMP = 'hexdump -v -e \'1/1 "0x%02x\\n"\''
36 
37     def __init__(self, os_if):
38         self.os_if = os_if
39         self._temp_path = '/var/tmp/faft/autest'
40         self._cbfs_work_path = os.path.join(self._temp_path, 'cbfs')
41         self._keys_path = os.path.join(self._temp_path, 'keys')
42         self._work_path = os.path.join(self._temp_path, 'work')
43         self._bios_path = 'bios.bin'
44         self._ec_path = 'ec.bin'
45         pubkey_path = os.path.join(self._keys_path, 'root_key.vbpubk')
46         self._bios_handler = common.LazyInitHandlerProxy(
47                 flashrom_handler.FlashromHandler,
48                 saft_flashrom_util,
49                 os_if,
50                 pubkey_path,
51                 self._keys_path,
52                 'bios')
53         self._ec_handler = common.LazyInitHandlerProxy(
54                 flashrom_handler.FlashromHandler,
55                 saft_flashrom_util,
56                 os_if,
57                 pubkey_path,
58                 self._keys_path,
59                 'ec')
60 
61         # _detect_image_paths always needs to run during initialization
62         # or after extract_shellball is called.
63         #
64         # If we are setting up the temp dir from scratch, we'll transitively
65         # call _detect_image_paths since extract_shellball is called.
66         # Otherwise, we need to scan the existing temp directory.
67         if not self.os_if.is_dir(self._temp_path):
68             self._setup_temp_dir()
69         else:
70             self._detect_image_paths()
71 
72     def _setup_temp_dir(self):
73         """Setup temporary directory.
74 
75         Devkeys are copied to _key_path. Then, shellball (default:
76         /usr/sbin/chromeos-firmwareupdate) is extracted to _work_path.
77         """
78         self.cleanup_temp_dir()
79 
80         self.os_if.create_dir(self._temp_path)
81         self.os_if.create_dir(self._cbfs_work_path)
82         self.os_if.create_dir(self._work_path)
83         self.os_if.copy_dir('/usr/share/vboot/devkeys', self._keys_path)
84 
85         original_shellball = '/usr/sbin/chromeos-firmwareupdate'
86         working_shellball = os.path.join(self._temp_path,
87                                          'chromeos-firmwareupdate')
88         self.os_if.copy_file(original_shellball, working_shellball)
89         self.extract_shellball()
90 
91     def cleanup_temp_dir(self):
92         """Cleanup temporary directory."""
93         if self.os_if.is_dir(self._temp_path):
94             self.os_if.remove_dir(self._temp_path)
95 
96     def stop_daemon(self):
97         """Stop update-engine daemon."""
98         self.os_if.log('Stopping %s...' % self.DAEMON)
99         cmd = 'status %s | grep stop || stop %s' % (self.DAEMON, self.DAEMON)
100         self.os_if.run_shell_command(cmd)
101 
102     def start_daemon(self):
103         """Start update-engine daemon."""
104         self.os_if.log('Starting %s...' % self.DAEMON)
105         cmd = 'status %s | grep start || start %s' % (self.DAEMON, self.DAEMON)
106         self.os_if.run_shell_command(cmd)
107 
108     def retrieve_fwid(self):
109         """Retrieve shellball's fwid tuple.
110 
111         This method should be called after _setup_temp_dir.
112 
113         Returns:
114             Shellball's fwid tuple (ro_fwid, rw_fwid).
115         """
116         self._bios_handler.new_image(
117                 os.path.join(self._work_path, self._bios_path))
118         # Remove the tailing null characters
119         ro_fwid = self._bios_handler.get_section_fwid('ro').rstrip('\0')
120         rw_fwid = self._bios_handler.get_section_fwid('a').rstrip('\0')
121         return (ro_fwid, rw_fwid)
122 
123     def retrieve_ecid(self):
124         """Retrieve shellball's ecid.
125 
126         This method should be called after _setup_temp_dir.
127 
128         Returns:
129             Shellball's ecid.
130         """
131         self._ec_handler.new_image(
132                 os.path.join(self._work_path, self._ec_path))
133         fwid = self._ec_handler.get_section_fwid('rw')
134         # Remove the tailing null characters
135         return fwid.rstrip('\0')
136 
137     def retrieve_ec_hash(self):
138         """Retrieve the hex string of the EC hash."""
139         return self._ec_handler.get_section_hash('rw')
140 
141     def modify_ecid_and_flash_to_bios(self):
142         """Modify ecid, put it to AP firmware, and flash it to the system.
143 
144         This method is used for testing EC software sync for EC EFS (Early
145         Firmware Selection). It creates a slightly different EC RW image
146         (a different EC fwid) in AP firmware, in order to trigger EC
147         software sync on the next boot (a different hash with the original
148         EC RW).
149 
150         The steps of this method:
151          * Modify the EC fwid by appending a '~', like from
152            'fizz_v1.1.7374-147f1bd64' to 'fizz_v1.1.7374-147f1bd64~'.
153          * Resign the EC image.
154          * Store the modififed EC RW image to CBFS component 'ecrw' of the
155            AP firmware's FW_MAIN_A and FW_MAIN_B, and also the new hash.
156          * Resign the AP image.
157          * Flash the modified AP image back to the system.
158         """
159         self.cbfs_setup_work_dir()
160 
161         fwid = self.retrieve_ecid()
162         if fwid.endswith('~'):
163             raise FirmwareUpdaterError('The EC fwid is already modified')
164 
165         # Modify the EC FWID and resign
166         fwid = fwid[:-1] + '~'
167         self._ec_handler.set_section_fwid('rw', fwid)
168         self._ec_handler.resign_ec_rwsig()
169 
170         # Replace ecrw to the new one
171         ecrw_bin_path = os.path.join(self._cbfs_work_path,
172                                      chip_utils.ecrw.cbfs_bin_name)
173         self._ec_handler.dump_section_body('rw', ecrw_bin_path)
174 
175         # Replace ecrw.hash to the new one
176         ecrw_hash_path = os.path.join(self._cbfs_work_path,
177                                       chip_utils.ecrw.cbfs_hash_name)
178         with open(ecrw_hash_path, 'w') as f:
179             f.write(self.retrieve_ec_hash())
180 
181         # Store the modified ecrw and its hash to cbfs
182         self.cbfs_replace_chip(chip_utils.ecrw.fw_name, extension='')
183 
184         # Resign and flash the AP firmware back to the system
185         self.cbfs_sign_and_flash()
186 
187     def resign_firmware(self, version=None, work_path=None):
188         """Resign firmware with version.
189 
190         Args:
191             version: new firmware version number, default to no modification.
192             work_path: work path, default to the updater work path.
193         """
194         if work_path is None:
195             work_path = self._work_path
196         self.os_if.run_shell_command(
197                 '/usr/share/vboot/bin/resign_firmwarefd.sh '
198                 '%s %s %s %s %s %s %s %s' % (
199                     os.path.join(work_path, self._bios_path),
200                     os.path.join(self._temp_path, 'output.bin'),
201                     os.path.join(self._keys_path, 'firmware_data_key.vbprivk'),
202                     os.path.join(self._keys_path, 'firmware.keyblock'),
203                     os.path.join(self._keys_path,
204                                  'dev_firmware_data_key.vbprivk'),
205                     os.path.join(self._keys_path, 'dev_firmware.keyblock'),
206                     os.path.join(self._keys_path, 'kernel_subkey.vbpubk'),
207                     ('%d' % version) if version is not None else ''))
208         self.os_if.copy_file('%s' % os.path.join(self._temp_path, 'output.bin'),
209                              '%s' % os.path.join(
210                                  work_path, self._bios_path))
211 
212     def _detect_image_paths(self):
213         """Scans shellball to find correct bios and ec image paths."""
214         def _extract_path_from_match(match_result, model):
215           """Extract a path from a matched line of setvars.sh.
216 
217           Args:
218             match_result: Match object: group 1 contains the quoted filename.
219             model: Name of model to use to resolve ${MODEL_DIR} in the filename.
220 
221           Returns:
222             pathname to firmware file (e.g. 'models/grunt/bios.bin').
223           """
224           pathname = match_result.group(1).replace('"', '')
225           pathname = pathname.replace('${MODEL_DIR}', 'models/' + model)
226           return pathname
227 
228         model_result = self.os_if.run_shell_command_get_output(
229             'mosys platform model')
230         if model_result:
231             model = model_result[0]
232             search_path = os.path.join(
233                 self._work_path, 'models', model, 'setvars.sh')
234             grep_result = self.os_if.run_shell_command_get_output(
235                 'grep IMAGE_MAIN= %s' % search_path)
236             if grep_result:
237                 match = re.match('IMAGE_MAIN=(.*)', grep_result[0])
238                 if match:
239                   self._bios_path = _extract_path_from_match(match, model)
240             grep_result = self.os_if.run_shell_command_get_output(
241                 'grep IMAGE_EC= %s' % search_path)
242             if grep_result:
243                 match = re.match('IMAGE_EC=(.*)', grep_result[0])
244                 if match:
245                   self._ec_path = _extract_path_from_match(match, model)
246 
247     def _update_target_fwid(self):
248         """Update target fwid/ecid in the setvars.sh."""
249         model_result = self.os_if.run_shell_command_get_output(
250             'mosys platform model')
251         if model_result:
252             model = model_result[0]
253             setvars_path = os.path.join(
254                 self._work_path, 'models', model, 'setvars.sh')
255             if self.os_if.path_exists(setvars_path):
256                 ro_fwid, rw_fwid = self.retrieve_fwid()
257                 args = ['-i']
258                 args.append(
259                     '"s/TARGET_FWID=\\".*\\"/TARGET_FWID=\\"%s\\"/g"'
260                     % rw_fwid)
261                 args.append(setvars_path)
262                 cmd = 'sed %s' % ' '.join(args)
263                 self.os_if.run_shell_command(cmd)
264 
265                 args = ['-i']
266                 args.append(
267                     '"s/TARGET_RO_FWID=\\".*\\"/TARGET_RO_FWID=\\"%s\\"/g"'
268                     % ro_fwid)
269                 args.append(setvars_path)
270                 cmd = 'sed %s' % ' '.join(args)
271                 self.os_if.run_shell_command(cmd)
272 
273                 # Only update ECID if an EC image is found
274                 if self.get_ec_relative_path():
275                     ecid = self.retrieve_ecid()
276                     args = ['-i']
277                     args.append(
278                         '"s/TARGET_ECID=\\".*\\"/TARGET_ECID=\\"%s\\"/g"'
279                         % ecid)
280                     args.append(setvars_path)
281                     cmd = 'sed %s' % ' '.join(args)
282                     self.os_if.run_shell_command(cmd)
283 
284     def extract_shellball(self, append=None):
285         """Extract the working shellball.
286 
287         Args:
288             append: decide which shellball to use with format
289                 chromeos-firmwareupdate-[append]. Use 'chromeos-firmwareupdate'
290                 if append is None.
291         """
292         working_shellball = os.path.join(self._temp_path,
293                                          'chromeos-firmwareupdate')
294         if append:
295             working_shellball = working_shellball + '-%s' % append
296 
297         self.os_if.run_shell_command('sh %s --sb_extract %s' % (
298                 working_shellball, self._work_path))
299 
300         self._detect_image_paths()
301 
302     def repack_shellball(self, append=None):
303         """Repack shellball with new fwid.
304 
305         New fwid follows the rule: [orignal_fwid]-[append].
306 
307         Args:
308             append: save the new shellball with a suffix, for example,
309                 chromeos-firmwareupdate-[append]. Use 'chromeos-firmwareupdate'
310                 if append is None.
311         """
312         self._update_target_fwid();
313 
314         working_shellball = os.path.join(self._temp_path,
315                                          'chromeos-firmwareupdate')
316         if append:
317             self.os_if.copy_file(working_shellball,
318                                  working_shellball + '-%s' % append)
319             working_shellball = working_shellball + '-%s' % append
320 
321         self.os_if.run_shell_command('sh %s --sb_repack %s' % (
322                 working_shellball, self._work_path))
323 
324         if append:
325             args = ['-i']
326             args.append(
327                     '"s/TARGET_FWID=\\"\\(.*\\)\\"/TARGET_FWID=\\"\\1.%s\\"/g"'
328                     % append)
329             args.append(working_shellball)
330             cmd = 'sed %s' % ' '.join(args)
331             self.os_if.run_shell_command(cmd)
332 
333             args = ['-i']
334             args.append('"s/TARGET_UNSTABLE=\\".*\\"/TARGET_UNSTABLE=\\"\\"/g"')
335             args.append(working_shellball)
336             cmd = 'sed %s' % ' '.join(args)
337             self.os_if.run_shell_command(cmd)
338 
339     def run_firmwareupdate(self, mode, updater_append=None, options=[]):
340         """Do firmwareupdate with updater in temp_dir.
341 
342         Args:
343             updater_append: decide which shellball to use with format
344                 chromeos-firmwareupdate-[append]. Use'chromeos-firmwareupdate'
345                 if updater_append is None.
346             mode: ex.'autoupdate', 'recovery', 'bootok', 'factory_install'...
347             options: ex. ['--noupdate_ec', '--force'] or [] for
348                 no option.
349         """
350         if updater_append:
351             updater = os.path.join(
352                 self._temp_path, 'chromeos-firmwareupdate-%s' % updater_append)
353         else:
354             updater = os.path.join(self._temp_path, 'chromeos-firmwareupdate')
355         command = '/bin/sh %s --mode %s %s' % (updater, mode, ' '.join(options))
356 
357         if mode == 'bootok':
358             # Since CL:459837, bootok is moved to chromeos-setgoodfirmware.
359             new_command = '/usr/sbin/chromeos-setgoodfirmware'
360             command = 'if [ -e %s ]; then %s; else %s; fi' % (
361                     new_command, new_command, command)
362 
363         self.os_if.run_shell_command(command)
364 
365     def cbfs_setup_work_dir(self):
366         """Sets up cbfs on DUT.
367 
368         Finds bios.bin on the DUT and sets up a temp dir to operate on
369         bios.bin.  If a bios.bin was specified, it is copied to the DUT
370         and used instead of the native bios.bin.
371 
372         Returns:
373             The cbfs work directory path.
374         """
375 
376         self.os_if.remove_dir(self._cbfs_work_path)
377         self.os_if.copy_dir(self._work_path, self._cbfs_work_path)
378 
379         return self._cbfs_work_path
380 
381     def cbfs_extract_chip(self, fw_name, extension='.bin'):
382         """Extracts chip firmware blob from cbfs.
383 
384         For a given chip type, looks for the corresponding firmware
385         blob and hash in the specified bios.  The firmware blob and
386         hash are extracted into self._cbfs_work_path.
387 
388         The extracted blobs will be <fw_name><extension> and
389         <fw_name>.hash located in cbfs_work_path.
390 
391         Args:
392             fw_name: Chip firmware name to be extracted.
393             extension: Extension of the name of the cbfs component.
394 
395         Returns:
396             Boolean success status.
397         """
398 
399         bios = os.path.join(self._cbfs_work_path, self._bios_path)
400         fw = fw_name
401         cbfs_extract = '%s %s extract -r FW_MAIN_A -n %s%%s -f %s%%s' % (
402             self.CBFSTOOL,
403             bios,
404             fw,
405             os.path.join(self._cbfs_work_path, fw))
406 
407         cmd = cbfs_extract % (extension, extension)
408         if self.os_if.run_shell_command_get_status(cmd) != 0:
409             return False
410 
411         cmd = cbfs_extract % ('.hash', '.hash')
412         if self.os_if.run_shell_command_get_status(cmd) != 0:
413             return False
414 
415         return True
416 
417     def cbfs_get_chip_hash(self, fw_name):
418         """Returns chip firmware hash blob.
419 
420         For a given chip type, returns the chip firmware hash blob.
421         Before making this request, the chip blobs must have been
422         extracted from cbfs using cbfs_extract_chip().
423         The hash data is returned as hexadecimal string.
424 
425         Args:
426             fw_name:
427                 Chip firmware name whose hash blob to get.
428 
429         Returns:
430             Boolean success status.
431 
432         Raises:
433             shell_wrapper.ShellError: Underlying remote shell
434                 operations failed.
435         """
436 
437         hexdump_cmd = '%s %s.hash' % (
438             self.HEXDUMP,
439             os.path.join(self._cbfs_work_path, fw_name))
440         hashblob = self.os_if.run_shell_command_get_output(hexdump_cmd)
441         return hashblob
442 
443     def cbfs_replace_chip(self, fw_name, extension='.bin'):
444         """Replaces chip firmware in CBFS (bios.bin).
445 
446         For a given chip type, replaces its firmware blob and hash in
447         bios.bin.  All files referenced are expected to be in the
448         directory set up using cbfs_setup_work_dir().
449 
450         Args:
451             fw_name: Chip firmware name to be replaced.
452             extension: Extension of the name of the cbfs component.
453 
454         Returns:
455             Boolean success status.
456 
457         Raises:
458             shell_wrapper.ShellError: Underlying remote shell
459                 operations failed.
460         """
461 
462         bios = os.path.join(self._cbfs_work_path, self._bios_path)
463         rm_hash_cmd = '%s %s remove -r FW_MAIN_A,FW_MAIN_B -n %s.hash' % (
464             self.CBFSTOOL, bios, fw_name)
465         rm_bin_cmd = '%s %s remove -r FW_MAIN_A,FW_MAIN_B -n %s%s' % (
466             self.CBFSTOOL, bios, fw_name, extension)
467         expand_cmd = '%s %s expand -r FW_MAIN_A,FW_MAIN_B' % (
468             self.CBFSTOOL, bios)
469         add_hash_cmd = ('%s %s add -r FW_MAIN_A,FW_MAIN_B -t raw -c none '
470                         '-f %s.hash -n %s.hash') % (
471                             self.CBFSTOOL,
472                             bios,
473                             os.path.join(self._cbfs_work_path, fw_name),
474                             fw_name)
475         add_bin_cmd = ('%s %s add -r FW_MAIN_A,FW_MAIN_B -t raw -c lzma '
476                        '-f %s%s -n %s%s') % (
477                            self.CBFSTOOL,
478                            bios,
479                            os.path.join(self._cbfs_work_path, fw_name),
480                            extension,
481                            fw_name,
482                            extension)
483         truncate_cmd = '%s %s truncate -r FW_MAIN_A,FW_MAIN_B' % (
484             self.CBFSTOOL, bios)
485 
486         self.os_if.run_shell_command(rm_hash_cmd)
487         self.os_if.run_shell_command(rm_bin_cmd)
488         try:
489             self.os_if.run_shell_command(expand_cmd)
490         except shell_wrapper.ShellError:
491             self.os_if.log(('%s may be too old, '
492                             'continuing without "expand" support') %
493                            self.CBFSTOOL)
494 
495         self.os_if.run_shell_command(add_hash_cmd)
496         self.os_if.run_shell_command(add_bin_cmd)
497         try:
498             self.os_if.run_shell_command(truncate_cmd)
499         except shell_wrapper.ShellError:
500             self.os_if.log(('%s may be too old, '
501                             'continuing without "truncate" support') %
502                            self.CBFSTOOL)
503 
504         return True
505 
506     def cbfs_sign_and_flash(self):
507         """Signs CBFS (bios.bin) and flashes it."""
508         self.resign_firmware(work_path=self._cbfs_work_path)
509         self._bios_handler.new_image(
510                 os.path.join(self._cbfs_work_path, self._bios_path))
511         self._bios_handler.write_whole()
512         return True
513 
514     def get_temp_path(self):
515         """Get temp directory path."""
516         return self._temp_path
517 
518     def get_keys_path(self):
519         """Get keys directory path."""
520         return self._keys_path
521 
522     def get_cbfs_work_path(self):
523         """Get cbfs work directory path."""
524         return self._cbfs_work_path
525 
526     def get_work_path(self):
527         """Get work directory path."""
528         return self._work_path
529 
530     def get_bios_relative_path(self):
531         """Gets the relative path of the bios image in the shellball."""
532         return self._bios_path
533 
534     def get_ec_relative_path(self):
535         """Gets the relative path of the ec image in the shellball."""
536         return self._ec_path
537