blob: 9448ef7dbc878f958b8708e1a0ac4f2e871688bd [file] [log] [blame]
Gilad Arnold553b0ec2013-01-26 01:00:39 -08001# Copyright (c) 2013 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"""Applying a Chrome OS update payload.
6
7This module is used internally by the main Payload class for applying an update
8payload. The interface for invoking the applier is as follows:
9
10 applier = PayloadApplier(payload)
11 applier.Run(...)
12
13"""
14
15import array
16import bz2
17import hashlib
Gilad Arnold658185a2013-05-08 17:57:54 -070018import itertools
Gilad Arnold553b0ec2013-01-26 01:00:39 -080019import os
20import shutil
21import subprocess
22import sys
23import tempfile
24
25import common
26from error import PayloadError
27
28
29#
30# Helper functions.
31#
Gilad Arnold382df5c2013-05-03 12:49:28 -070032def _VerifySha256(file_obj, expected_hash, name, length=-1):
Gilad Arnold553b0ec2013-01-26 01:00:39 -080033 """Verifies the SHA256 hash of a file.
34
35 Args:
36 file_obj: file object to read
37 expected_hash: the hash digest we expect to be getting
38 name: name string of this hash, for error reporting
Gilad Arnold382df5c2013-05-03 12:49:28 -070039 length: precise length of data to verify (optional)
Gilad Arnold553b0ec2013-01-26 01:00:39 -080040 Raises:
Gilad Arnold382df5c2013-05-03 12:49:28 -070041 PayloadError if computed hash doesn't match expected one, or if fails to
42 read the specified length of data.
Gilad Arnold553b0ec2013-01-26 01:00:39 -080043
44 """
45 # pylint: disable=E1101
46 hasher = hashlib.sha256()
47 block_length = 1024 * 1024
Gilad Arnold382df5c2013-05-03 12:49:28 -070048 max_length = length if length >= 0 else sys.maxint
Gilad Arnold553b0ec2013-01-26 01:00:39 -080049
Gilad Arnold382df5c2013-05-03 12:49:28 -070050 while max_length > 0:
Gilad Arnold553b0ec2013-01-26 01:00:39 -080051 read_length = min(max_length, block_length)
52 data = file_obj.read(read_length)
53 if not data:
54 break
55 max_length -= len(data)
56 hasher.update(data)
57
Gilad Arnold382df5c2013-05-03 12:49:28 -070058 if length >= 0 and max_length > 0:
59 raise PayloadError(
60 'insufficient data (%d instead of %d) when verifying %s' %
61 (length - max_length, length, name))
62
Gilad Arnold553b0ec2013-01-26 01:00:39 -080063 actual_hash = hasher.digest()
64 if actual_hash != expected_hash:
65 raise PayloadError('%s hash (%s) not as expected (%s)' %
Gilad Arnold96405372013-05-04 00:24:58 -070066 (name, common.FormatSha256(actual_hash),
67 common.FormatSha256(expected_hash)))
Gilad Arnold553b0ec2013-01-26 01:00:39 -080068
69
70def _ReadExtents(file_obj, extents, block_size, max_length=-1):
71 """Reads data from file as defined by extent sequence.
72
73 This tries to be efficient by not copying data as it is read in chunks.
74
75 Args:
76 file_obj: file object
77 extents: sequence of block extents (offset and length)
78 block_size: size of each block
79 max_length: maximum length to read (optional)
80 Returns:
81 A character array containing the concatenated read data.
82
83 """
84 data = array.array('c')
Gilad Arnold272a4992013-05-08 13:12:53 -070085 if max_length < 0:
86 max_length = sys.maxint
Gilad Arnold553b0ec2013-01-26 01:00:39 -080087 for ex in extents:
88 if max_length == 0:
89 break
Gilad Arnold272a4992013-05-08 13:12:53 -070090 read_length = min(max_length, ex.num_blocks * block_size)
Gilad Arnold658185a2013-05-08 17:57:54 -070091
92 # Fill with zeros or read from file, depending on the type of extent.
93 if ex.start_block == common.PSEUDO_EXTENT_MARKER:
94 data.extend(itertools.repeat('\0', read_length))
95 else:
96 file_obj.seek(ex.start_block * block_size)
97 data.fromfile(file_obj, read_length)
98
Gilad Arnold272a4992013-05-08 13:12:53 -070099 max_length -= read_length
Gilad Arnold658185a2013-05-08 17:57:54 -0700100
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800101 return data
102
103
104def _WriteExtents(file_obj, data, extents, block_size, base_name):
Gilad Arnold272a4992013-05-08 13:12:53 -0700105 """Writes data to file as defined by extent sequence.
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800106
107 This tries to be efficient by not copy data as it is written in chunks.
108
109 Args:
110 file_obj: file object
111 data: data to write
112 extents: sequence of block extents (offset and length)
113 block_size: size of each block
Gilad Arnold272a4992013-05-08 13:12:53 -0700114 base_name: name string of extent sequence for error reporting
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800115 Raises:
116 PayloadError when things don't add up.
117
118 """
119 data_offset = 0
120 data_length = len(data)
121 for ex, ex_name in common.ExtentIter(extents, base_name):
Gilad Arnold272a4992013-05-08 13:12:53 -0700122 if not data_length:
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800123 raise PayloadError('%s: more write extents than data' % ex_name)
Gilad Arnold272a4992013-05-08 13:12:53 -0700124 write_length = min(data_length, ex.num_blocks * block_size)
Gilad Arnold658185a2013-05-08 17:57:54 -0700125
126 # Only do actual writing if this is not a pseudo-extent.
127 if ex.start_block != common.PSEUDO_EXTENT_MARKER:
128 file_obj.seek(ex.start_block * block_size)
129 data_view = buffer(data, data_offset, write_length)
130 file_obj.write(data_view)
131
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800132 data_offset += write_length
Gilad Arnold272a4992013-05-08 13:12:53 -0700133 data_length -= write_length
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800134
Gilad Arnold272a4992013-05-08 13:12:53 -0700135 if data_length:
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800136 raise PayloadError('%s: more data than write extents' % base_name)
137
138
Gilad Arnold272a4992013-05-08 13:12:53 -0700139def _ExtentsToBspatchArg(extents, block_size, base_name, data_length=-1):
140 """Translates an extent sequence into a bspatch-compatible string argument.
141
142 Args:
143 extents: sequence of block extents (offset and length)
144 block_size: size of each block
145 base_name: name string of extent sequence for error reporting
146 data_length: the actual total length of the data in bytes (optional)
147 Returns:
148 A tuple consisting of (i) a string of the form
149 "off_1:len_1,...,off_n:len_n", (ii) an offset where zero padding is needed
150 for filling the last extent, (iii) the length of the padding (zero means no
151 padding is needed and the extents cover the full length of data).
152 Raises:
153 PayloadError if data_length is too short or too long.
154
155 """
156 arg = ''
157 pad_off = pad_len = 0
158 if data_length < 0:
159 data_length = sys.maxint
160 for ex, ex_name in common.ExtentIter(extents, base_name):
161 if not data_length:
162 raise PayloadError('%s: more extents than total data length' % ex_name)
Gilad Arnold658185a2013-05-08 17:57:54 -0700163
164 is_pseudo = ex.start_block == common.PSEUDO_EXTENT_MARKER
165 start_byte = -1 if is_pseudo else ex.start_block * block_size
Gilad Arnold272a4992013-05-08 13:12:53 -0700166 num_bytes = ex.num_blocks * block_size
167 if data_length < num_bytes:
Gilad Arnold658185a2013-05-08 17:57:54 -0700168 # We're only padding a real extent.
169 if not is_pseudo:
170 pad_off = start_byte + data_length
171 pad_len = num_bytes - data_length
172
Gilad Arnold272a4992013-05-08 13:12:53 -0700173 num_bytes = data_length
Gilad Arnold658185a2013-05-08 17:57:54 -0700174
Gilad Arnold272a4992013-05-08 13:12:53 -0700175 arg += '%s%d:%d' % (arg and ',', start_byte, num_bytes)
176 data_length -= num_bytes
177
178 if data_length:
179 raise PayloadError('%s: extents not covering full data length' % base_name)
180
181 return arg, pad_off, pad_len
182
183
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800184#
185# Payload application.
186#
187class PayloadApplier(object):
188 """Applying an update payload.
189
190 This is a short-lived object whose purpose is to isolate the logic used for
191 applying an update payload.
192
193 """
194
Gilad Arnold21a02502013-08-22 16:59:48 -0700195 def __init__(self, payload, bsdiff_in_place=True, bspatch_path=None,
Gilad Arnolde5fdf182013-05-23 16:13:38 -0700196 truncate_to_expected_size=True):
Gilad Arnold272a4992013-05-08 13:12:53 -0700197 """Initialize the applier.
198
199 Args:
200 payload: the payload object to check
201 bsdiff_in_place: whether to perform BSDIFF operation in-place (optional)
Gilad Arnold21a02502013-08-22 16:59:48 -0700202 bspatch_path: path to the bspatch binary (optional)
Gilad Arnolde5fdf182013-05-23 16:13:38 -0700203 truncate_to_expected_size: whether to truncate the resulting partitions
204 to their expected sizes, as specified in the
205 payload (optional)
Gilad Arnold272a4992013-05-08 13:12:53 -0700206
207 """
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800208 assert payload.is_init, 'uninitialized update payload'
209 self.payload = payload
210 self.block_size = payload.manifest.block_size
Gilad Arnold272a4992013-05-08 13:12:53 -0700211 self.bsdiff_in_place = bsdiff_in_place
Gilad Arnold21a02502013-08-22 16:59:48 -0700212 self.bspatch_path = bspatch_path or 'bspatch'
Gilad Arnolde5fdf182013-05-23 16:13:38 -0700213 self.truncate_to_expected_size = truncate_to_expected_size
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800214
215 def _ApplyReplaceOperation(self, op, op_name, out_data, part_file, part_size):
216 """Applies a REPLACE{,_BZ} operation.
217
218 Args:
219 op: the operation object
220 op_name: name string for error reporting
221 out_data: the data to be written
222 part_file: the partition file object
223 part_size: the size of the partition
224 Raises:
225 PayloadError if something goes wrong.
226
227 """
228 block_size = self.block_size
229 data_length = len(out_data)
230
231 # Decompress data if needed.
232 if op.type == common.OpType.REPLACE_BZ:
233 out_data = bz2.decompress(out_data)
234 data_length = len(out_data)
235
236 # Write data to blocks specified in dst extents.
237 data_start = 0
238 for ex, ex_name in common.ExtentIter(op.dst_extents,
239 '%s.dst_extents' % op_name):
240 start_block = ex.start_block
241 num_blocks = ex.num_blocks
242 count = num_blocks * block_size
243
244 # Make sure it's not a fake (signature) operation.
245 if start_block != common.PSEUDO_EXTENT_MARKER:
246 data_end = data_start + count
247
248 # Make sure we're not running past partition boundary.
249 if (start_block + num_blocks) * block_size > part_size:
250 raise PayloadError(
251 '%s: extent (%s) exceeds partition size (%d)' %
252 (ex_name, common.FormatExtent(ex, block_size),
253 part_size))
254
255 # Make sure that we have enough data to write.
256 if data_end >= data_length + block_size:
257 raise PayloadError(
258 '%s: more dst blocks than data (even with padding)')
259
260 # Pad with zeros if necessary.
261 if data_end > data_length:
262 padding = data_end - data_length
263 out_data += '\0' * padding
264
265 self.payload.payload_file.seek(start_block * block_size)
266 part_file.seek(start_block * block_size)
267 part_file.write(out_data[data_start:data_end])
268
269 data_start += count
270
271 # Make sure we wrote all data.
272 if data_start < data_length:
273 raise PayloadError('%s: wrote fewer bytes (%d) than expected (%d)' %
274 (op_name, data_start, data_length))
275
276 def _ApplyMoveOperation(self, op, op_name, part_file):
277 """Applies a MOVE operation.
278
Gilad Arnold658185a2013-05-08 17:57:54 -0700279 Note that this operation must read the whole block data from the input and
280 only then dump it, due to our in-place update semantics; otherwise, it
281 might clobber data midway through.
282
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800283 Args:
284 op: the operation object
285 op_name: name string for error reporting
286 part_file: the partition file object
287 Raises:
288 PayloadError if something goes wrong.
289
290 """
291 block_size = self.block_size
292
293 # Gather input raw data from src extents.
294 in_data = _ReadExtents(part_file, op.src_extents, block_size)
295
296 # Dump extracted data to dst extents.
297 _WriteExtents(part_file, in_data, op.dst_extents, block_size,
298 '%s.dst_extents' % op_name)
299
300 def _ApplyBsdiffOperation(self, op, op_name, patch_data, part_file):
301 """Applies a BSDIFF operation.
302
303 Args:
304 op: the operation object
305 op_name: name string for error reporting
306 patch_data: the binary patch content
307 part_file: the partition file object
308 Raises:
309 PayloadError if something goes wrong.
310
311 """
312 block_size = self.block_size
313
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800314 # Dump patch data to file.
315 with tempfile.NamedTemporaryFile(delete=False) as patch_file:
316 patch_file_name = patch_file.name
317 patch_file.write(patch_data)
318
Gilad Arnold272a4992013-05-08 13:12:53 -0700319 if self.bsdiff_in_place and hasattr(part_file, 'fileno'):
320 # Construct input and output extents argument for bspatch.
321 in_extents_arg, _, _ = _ExtentsToBspatchArg(
322 op.src_extents, block_size, '%s.src_extents' % op_name,
323 data_length=op.src_length)
324 out_extents_arg, pad_off, pad_len = _ExtentsToBspatchArg(
325 op.dst_extents, block_size, '%s.dst_extents' % op_name,
326 data_length=op.dst_length)
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800327
Gilad Arnold272a4992013-05-08 13:12:53 -0700328 # Invoke bspatch on partition file with extents args.
329 file_name = '/dev/fd/%d' % part_file.fileno()
Gilad Arnold21a02502013-08-22 16:59:48 -0700330 bspatch_cmd = [self.bspatch_path, file_name, file_name, patch_file_name,
Gilad Arnold272a4992013-05-08 13:12:53 -0700331 in_extents_arg, out_extents_arg]
332 subprocess.check_call(bspatch_cmd)
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800333
Gilad Arnold272a4992013-05-08 13:12:53 -0700334 # Pad with zeros past the total output length.
335 if pad_len:
336 part_file.seek(pad_off)
337 part_file.write('\0' * pad_len)
338 else:
339 # Gather input raw data and write to a temp file.
340 in_data = _ReadExtents(part_file, op.src_extents, block_size,
341 max_length=op.src_length)
342 with tempfile.NamedTemporaryFile(delete=False) as in_file:
343 in_file_name = in_file.name
344 in_file.write(in_data)
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800345
Gilad Arnold272a4992013-05-08 13:12:53 -0700346 # Allocate tepmorary output file.
347 with tempfile.NamedTemporaryFile(delete=False) as out_file:
348 out_file_name = out_file.name
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800349
Gilad Arnold272a4992013-05-08 13:12:53 -0700350 # Invoke bspatch.
Gilad Arnold21a02502013-08-22 16:59:48 -0700351 bspatch_cmd = [self.bspatch_path, in_file_name, out_file_name,
352 patch_file_name]
Gilad Arnold272a4992013-05-08 13:12:53 -0700353 subprocess.check_call(bspatch_cmd)
354
355 # Read output.
356 with open(out_file_name, 'rb') as out_file:
357 out_data = out_file.read()
358 if len(out_data) != op.dst_length:
359 raise PayloadError(
360 '%s: actual patched data length (%d) not as expected (%d)' %
361 (op_name, len(out_data), op.dst_length))
362
363 # Write output back to partition, with padding.
364 unaligned_out_len = len(out_data) % block_size
365 if unaligned_out_len:
366 out_data += '\0' * (block_size - unaligned_out_len)
367 _WriteExtents(part_file, out_data, op.dst_extents, block_size,
368 '%s.dst_extents' % op_name)
369
370 # Delete input/output files.
371 os.remove(in_file_name)
372 os.remove(out_file_name)
373
374 # Delete patch file.
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800375 os.remove(patch_file_name)
376
377 def _ApplyOperations(self, operations, base_name, part_file, part_size):
378 """Applies a sequence of update operations to a partition.
379
380 This assumes an in-place update semantics, namely all reads are performed
381 first, then the data is processed and written back to the same file.
382
383 Args:
384 operations: the sequence of operations
385 base_name: the name of the operation sequence
386 part_file: the partition file object, open for reading/writing
387 part_size: the partition size
388 Raises:
389 PayloadError if anything goes wrong while processing the payload.
390
391 """
392 for op, op_name in common.OperationIter(operations, base_name):
393 # Read data blob.
394 data = self.payload.ReadDataBlob(op.data_offset, op.data_length)
395
396 if op.type in (common.OpType.REPLACE, common.OpType.REPLACE_BZ):
397 self._ApplyReplaceOperation(op, op_name, data, part_file, part_size)
398 elif op.type == common.OpType.MOVE:
399 self._ApplyMoveOperation(op, op_name, part_file)
400 elif op.type == common.OpType.BSDIFF:
401 self._ApplyBsdiffOperation(op, op_name, data, part_file)
402 else:
403 raise PayloadError('%s: unknown operation type (%d)' %
404 (op_name, op.type))
405
406 def _ApplyToPartition(self, operations, part_name, base_name,
Gilad Arnold16416602013-05-04 21:40:39 -0700407 new_part_file_name, new_part_info,
408 old_part_file_name=None, old_part_info=None):
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800409 """Applies an update to a partition.
410
411 Args:
412 operations: the sequence of update operations to apply
413 part_name: the name of the partition, for error reporting
414 base_name: the name of the operation sequence
Gilad Arnold16416602013-05-04 21:40:39 -0700415 new_part_file_name: file name to write partition data to
416 new_part_info: size and expected hash of dest partition
417 old_part_file_name: file name of source partition (optional)
418 old_part_info: size and expected hash of source partition (optional)
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800419 Raises:
420 PayloadError if anything goes wrong with the update.
421
422 """
423 # Do we have a source partition?
Gilad Arnold16416602013-05-04 21:40:39 -0700424 if old_part_file_name:
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800425 # Verify the source partition.
Gilad Arnold16416602013-05-04 21:40:39 -0700426 with open(old_part_file_name, 'rb') as old_part_file:
427 _VerifySha256(old_part_file, old_part_info.hash, part_name,
428 length=old_part_info.size)
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800429
Gilad Arnoldf69065c2013-05-27 16:54:59 -0700430 # Copy the src partition to the dst one; make sure we don't truncate it.
Gilad Arnold16416602013-05-04 21:40:39 -0700431 shutil.copyfile(old_part_file_name, new_part_file_name)
Gilad Arnoldf69065c2013-05-27 16:54:59 -0700432 new_part_file_mode = 'r+b'
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800433 else:
Gilad Arnoldf69065c2013-05-27 16:54:59 -0700434 # We need to create/truncate the dst partition file.
435 new_part_file_mode = 'w+b'
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800436
437 # Apply operations.
Gilad Arnoldf69065c2013-05-27 16:54:59 -0700438 with open(new_part_file_name, new_part_file_mode) as new_part_file:
Gilad Arnold16416602013-05-04 21:40:39 -0700439 self._ApplyOperations(operations, base_name, new_part_file,
440 new_part_info.size)
Gilad Arnolde5fdf182013-05-23 16:13:38 -0700441 # Truncate the result, if so instructed.
442 if self.truncate_to_expected_size:
443 new_part_file.seek(0, 2)
444 if new_part_file.tell() > new_part_info.size:
445 new_part_file.seek(new_part_info.size)
446 new_part_file.truncate()
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800447
448 # Verify the resulting partition.
Gilad Arnold16416602013-05-04 21:40:39 -0700449 with open(new_part_file_name, 'rb') as new_part_file:
450 _VerifySha256(new_part_file, new_part_info.hash, part_name,
451 length=new_part_info.size)
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800452
Gilad Arnold16416602013-05-04 21:40:39 -0700453 def Run(self, new_kernel_part, new_rootfs_part, old_kernel_part=None,
454 old_rootfs_part=None):
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800455 """Applier entry point, invoking all update operations.
456
457 Args:
Gilad Arnold16416602013-05-04 21:40:39 -0700458 new_kernel_part: name of dest kernel partition file
459 new_rootfs_part: name of dest rootfs partition file
460 old_kernel_part: name of source kernel partition file (optional)
461 old_rootfs_part: name of source rootfs partition file (optional)
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800462 Raises:
463 PayloadError if payload application failed.
464
465 """
466 self.payload.ResetFile()
467
468 # Make sure the arguments are sane and match the payload.
Gilad Arnold16416602013-05-04 21:40:39 -0700469 if not (new_kernel_part and new_rootfs_part):
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800470 raise PayloadError('missing dst {kernel,rootfs} partitions')
471
Gilad Arnold16416602013-05-04 21:40:39 -0700472 if not (old_kernel_part or old_rootfs_part):
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800473 if not self.payload.IsFull():
474 raise PayloadError('trying to apply a non-full update without src '
475 '{kernel,rootfs} partitions')
Gilad Arnold16416602013-05-04 21:40:39 -0700476 elif old_kernel_part and old_rootfs_part:
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800477 if not self.payload.IsDelta():
478 raise PayloadError('trying to apply a non-delta update onto src '
479 '{kernel,rootfs} partitions')
480 else:
481 raise PayloadError('not all src partitions provided')
482
483 # Apply update to rootfs.
484 self._ApplyToPartition(
485 self.payload.manifest.install_operations, 'rootfs',
Gilad Arnold16416602013-05-04 21:40:39 -0700486 'install_operations', new_rootfs_part,
487 self.payload.manifest.new_rootfs_info, old_rootfs_part,
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800488 self.payload.manifest.old_rootfs_info)
489
490 # Apply update to kernel update.
491 self._ApplyToPartition(
492 self.payload.manifest.kernel_install_operations, 'kernel',
Gilad Arnold16416602013-05-04 21:40:39 -0700493 'kernel_install_operations', new_kernel_part,
494 self.payload.manifest.new_kernel_info, old_kernel_part,
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800495 self.payload.manifest.old_kernel_info)