blob: 85f8b0ebdeab6256014ba511d2e230af798975c9 [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 Arnolde5fdf182013-05-23 16:13:38 -0700195 def __init__(self, payload, bsdiff_in_place=True,
196 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 Arnolde5fdf182013-05-23 16:13:38 -0700202 truncate_to_expected_size: whether to truncate the resulting partitions
203 to their expected sizes, as specified in the
204 payload (optional)
Gilad Arnold272a4992013-05-08 13:12:53 -0700205
206 """
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800207 assert payload.is_init, 'uninitialized update payload'
208 self.payload = payload
209 self.block_size = payload.manifest.block_size
Gilad Arnold272a4992013-05-08 13:12:53 -0700210 self.bsdiff_in_place = bsdiff_in_place
Gilad Arnolde5fdf182013-05-23 16:13:38 -0700211 self.truncate_to_expected_size = truncate_to_expected_size
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800212
213 def _ApplyReplaceOperation(self, op, op_name, out_data, part_file, part_size):
214 """Applies a REPLACE{,_BZ} operation.
215
216 Args:
217 op: the operation object
218 op_name: name string for error reporting
219 out_data: the data to be written
220 part_file: the partition file object
221 part_size: the size of the partition
222 Raises:
223 PayloadError if something goes wrong.
224
225 """
226 block_size = self.block_size
227 data_length = len(out_data)
228
229 # Decompress data if needed.
230 if op.type == common.OpType.REPLACE_BZ:
231 out_data = bz2.decompress(out_data)
232 data_length = len(out_data)
233
234 # Write data to blocks specified in dst extents.
235 data_start = 0
236 for ex, ex_name in common.ExtentIter(op.dst_extents,
237 '%s.dst_extents' % op_name):
238 start_block = ex.start_block
239 num_blocks = ex.num_blocks
240 count = num_blocks * block_size
241
242 # Make sure it's not a fake (signature) operation.
243 if start_block != common.PSEUDO_EXTENT_MARKER:
244 data_end = data_start + count
245
246 # Make sure we're not running past partition boundary.
247 if (start_block + num_blocks) * block_size > part_size:
248 raise PayloadError(
249 '%s: extent (%s) exceeds partition size (%d)' %
250 (ex_name, common.FormatExtent(ex, block_size),
251 part_size))
252
253 # Make sure that we have enough data to write.
254 if data_end >= data_length + block_size:
255 raise PayloadError(
256 '%s: more dst blocks than data (even with padding)')
257
258 # Pad with zeros if necessary.
259 if data_end > data_length:
260 padding = data_end - data_length
261 out_data += '\0' * padding
262
263 self.payload.payload_file.seek(start_block * block_size)
264 part_file.seek(start_block * block_size)
265 part_file.write(out_data[data_start:data_end])
266
267 data_start += count
268
269 # Make sure we wrote all data.
270 if data_start < data_length:
271 raise PayloadError('%s: wrote fewer bytes (%d) than expected (%d)' %
272 (op_name, data_start, data_length))
273
274 def _ApplyMoveOperation(self, op, op_name, part_file):
275 """Applies a MOVE operation.
276
Gilad Arnold658185a2013-05-08 17:57:54 -0700277 Note that this operation must read the whole block data from the input and
278 only then dump it, due to our in-place update semantics; otherwise, it
279 might clobber data midway through.
280
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800281 Args:
282 op: the operation object
283 op_name: name string for error reporting
284 part_file: the partition file object
285 Raises:
286 PayloadError if something goes wrong.
287
288 """
289 block_size = self.block_size
290
291 # Gather input raw data from src extents.
292 in_data = _ReadExtents(part_file, op.src_extents, block_size)
293
294 # Dump extracted data to dst extents.
295 _WriteExtents(part_file, in_data, op.dst_extents, block_size,
296 '%s.dst_extents' % op_name)
297
298 def _ApplyBsdiffOperation(self, op, op_name, patch_data, part_file):
299 """Applies a BSDIFF operation.
300
301 Args:
302 op: the operation object
303 op_name: name string for error reporting
304 patch_data: the binary patch content
305 part_file: the partition file object
306 Raises:
307 PayloadError if something goes wrong.
308
309 """
310 block_size = self.block_size
311
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800312 # Dump patch data to file.
313 with tempfile.NamedTemporaryFile(delete=False) as patch_file:
314 patch_file_name = patch_file.name
315 patch_file.write(patch_data)
316
Gilad Arnold272a4992013-05-08 13:12:53 -0700317 if self.bsdiff_in_place and hasattr(part_file, 'fileno'):
318 # Construct input and output extents argument for bspatch.
319 in_extents_arg, _, _ = _ExtentsToBspatchArg(
320 op.src_extents, block_size, '%s.src_extents' % op_name,
321 data_length=op.src_length)
322 out_extents_arg, pad_off, pad_len = _ExtentsToBspatchArg(
323 op.dst_extents, block_size, '%s.dst_extents' % op_name,
324 data_length=op.dst_length)
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800325
Gilad Arnold272a4992013-05-08 13:12:53 -0700326 # Invoke bspatch on partition file with extents args.
327 file_name = '/dev/fd/%d' % part_file.fileno()
328 bspatch_cmd = ['bspatch', file_name, file_name, patch_file_name,
329 in_extents_arg, out_extents_arg]
330 subprocess.check_call(bspatch_cmd)
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800331
Gilad Arnold272a4992013-05-08 13:12:53 -0700332 # Pad with zeros past the total output length.
333 if pad_len:
334 part_file.seek(pad_off)
335 part_file.write('\0' * pad_len)
336 else:
337 # Gather input raw data and write to a temp file.
338 in_data = _ReadExtents(part_file, op.src_extents, block_size,
339 max_length=op.src_length)
340 with tempfile.NamedTemporaryFile(delete=False) as in_file:
341 in_file_name = in_file.name
342 in_file.write(in_data)
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800343
Gilad Arnold272a4992013-05-08 13:12:53 -0700344 # Allocate tepmorary output file.
345 with tempfile.NamedTemporaryFile(delete=False) as out_file:
346 out_file_name = out_file.name
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800347
Gilad Arnold272a4992013-05-08 13:12:53 -0700348 # Invoke bspatch.
349 bspatch_cmd = ['bspatch', in_file_name, out_file_name, patch_file_name]
350 subprocess.check_call(bspatch_cmd)
351
352 # Read output.
353 with open(out_file_name, 'rb') as out_file:
354 out_data = out_file.read()
355 if len(out_data) != op.dst_length:
356 raise PayloadError(
357 '%s: actual patched data length (%d) not as expected (%d)' %
358 (op_name, len(out_data), op.dst_length))
359
360 # Write output back to partition, with padding.
361 unaligned_out_len = len(out_data) % block_size
362 if unaligned_out_len:
363 out_data += '\0' * (block_size - unaligned_out_len)
364 _WriteExtents(part_file, out_data, op.dst_extents, block_size,
365 '%s.dst_extents' % op_name)
366
367 # Delete input/output files.
368 os.remove(in_file_name)
369 os.remove(out_file_name)
370
371 # Delete patch file.
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800372 os.remove(patch_file_name)
373
374 def _ApplyOperations(self, operations, base_name, part_file, part_size):
375 """Applies a sequence of update operations to a partition.
376
377 This assumes an in-place update semantics, namely all reads are performed
378 first, then the data is processed and written back to the same file.
379
380 Args:
381 operations: the sequence of operations
382 base_name: the name of the operation sequence
383 part_file: the partition file object, open for reading/writing
384 part_size: the partition size
385 Raises:
386 PayloadError if anything goes wrong while processing the payload.
387
388 """
389 for op, op_name in common.OperationIter(operations, base_name):
390 # Read data blob.
391 data = self.payload.ReadDataBlob(op.data_offset, op.data_length)
392
393 if op.type in (common.OpType.REPLACE, common.OpType.REPLACE_BZ):
394 self._ApplyReplaceOperation(op, op_name, data, part_file, part_size)
395 elif op.type == common.OpType.MOVE:
396 self._ApplyMoveOperation(op, op_name, part_file)
397 elif op.type == common.OpType.BSDIFF:
398 self._ApplyBsdiffOperation(op, op_name, data, part_file)
399 else:
400 raise PayloadError('%s: unknown operation type (%d)' %
401 (op_name, op.type))
402
403 def _ApplyToPartition(self, operations, part_name, base_name,
Gilad Arnold16416602013-05-04 21:40:39 -0700404 new_part_file_name, new_part_info,
405 old_part_file_name=None, old_part_info=None):
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800406 """Applies an update to a partition.
407
408 Args:
409 operations: the sequence of update operations to apply
410 part_name: the name of the partition, for error reporting
411 base_name: the name of the operation sequence
Gilad Arnold16416602013-05-04 21:40:39 -0700412 new_part_file_name: file name to write partition data to
413 new_part_info: size and expected hash of dest partition
414 old_part_file_name: file name of source partition (optional)
415 old_part_info: size and expected hash of source partition (optional)
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800416 Raises:
417 PayloadError if anything goes wrong with the update.
418
419 """
420 # Do we have a source partition?
Gilad Arnold16416602013-05-04 21:40:39 -0700421 if old_part_file_name:
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800422 # Verify the source partition.
Gilad Arnold16416602013-05-04 21:40:39 -0700423 with open(old_part_file_name, 'rb') as old_part_file:
424 _VerifySha256(old_part_file, old_part_info.hash, part_name,
425 length=old_part_info.size)
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800426
Gilad Arnoldf69065c2013-05-27 16:54:59 -0700427 # Copy the src partition to the dst one; make sure we don't truncate it.
Gilad Arnold16416602013-05-04 21:40:39 -0700428 shutil.copyfile(old_part_file_name, new_part_file_name)
Gilad Arnoldf69065c2013-05-27 16:54:59 -0700429 new_part_file_mode = 'r+b'
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800430 else:
Gilad Arnoldf69065c2013-05-27 16:54:59 -0700431 # We need to create/truncate the dst partition file.
432 new_part_file_mode = 'w+b'
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800433
434 # Apply operations.
Gilad Arnoldf69065c2013-05-27 16:54:59 -0700435 with open(new_part_file_name, new_part_file_mode) as new_part_file:
Gilad Arnold16416602013-05-04 21:40:39 -0700436 self._ApplyOperations(operations, base_name, new_part_file,
437 new_part_info.size)
Gilad Arnolde5fdf182013-05-23 16:13:38 -0700438 # Truncate the result, if so instructed.
439 if self.truncate_to_expected_size:
440 new_part_file.seek(0, 2)
441 if new_part_file.tell() > new_part_info.size:
442 new_part_file.seek(new_part_info.size)
443 new_part_file.truncate()
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800444
445 # Verify the resulting partition.
Gilad Arnold16416602013-05-04 21:40:39 -0700446 with open(new_part_file_name, 'rb') as new_part_file:
447 _VerifySha256(new_part_file, new_part_info.hash, part_name,
448 length=new_part_info.size)
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800449
Gilad Arnold16416602013-05-04 21:40:39 -0700450 def Run(self, new_kernel_part, new_rootfs_part, old_kernel_part=None,
451 old_rootfs_part=None):
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800452 """Applier entry point, invoking all update operations.
453
454 Args:
Gilad Arnold16416602013-05-04 21:40:39 -0700455 new_kernel_part: name of dest kernel partition file
456 new_rootfs_part: name of dest rootfs partition file
457 old_kernel_part: name of source kernel partition file (optional)
458 old_rootfs_part: name of source rootfs partition file (optional)
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800459 Raises:
460 PayloadError if payload application failed.
461
462 """
463 self.payload.ResetFile()
464
465 # Make sure the arguments are sane and match the payload.
Gilad Arnold16416602013-05-04 21:40:39 -0700466 if not (new_kernel_part and new_rootfs_part):
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800467 raise PayloadError('missing dst {kernel,rootfs} partitions')
468
Gilad Arnold16416602013-05-04 21:40:39 -0700469 if not (old_kernel_part or old_rootfs_part):
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800470 if not self.payload.IsFull():
471 raise PayloadError('trying to apply a non-full update without src '
472 '{kernel,rootfs} partitions')
Gilad Arnold16416602013-05-04 21:40:39 -0700473 elif old_kernel_part and old_rootfs_part:
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800474 if not self.payload.IsDelta():
475 raise PayloadError('trying to apply a non-delta update onto src '
476 '{kernel,rootfs} partitions')
477 else:
478 raise PayloadError('not all src partitions provided')
479
480 # Apply update to rootfs.
481 self._ApplyToPartition(
482 self.payload.manifest.install_operations, 'rootfs',
Gilad Arnold16416602013-05-04 21:40:39 -0700483 'install_operations', new_rootfs_part,
484 self.payload.manifest.new_rootfs_info, old_rootfs_part,
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800485 self.payload.manifest.old_rootfs_info)
486
487 # Apply update to kernel update.
488 self._ApplyToPartition(
489 self.payload.manifest.kernel_install_operations, 'kernel',
Gilad Arnold16416602013-05-04 21:40:39 -0700490 'kernel_install_operations', new_kernel_part,
491 self.payload.manifest.new_kernel_info, old_kernel_part,
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800492 self.payload.manifest.old_kernel_info)