blob: f5ae6b732650a5922a17fe304665ae93b447bdf6 [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 Arnold272a4992013-05-08 13:12:53 -0700195 def __init__(self, payload, bsdiff_in_place=True):
196 """Initialize the applier.
197
198 Args:
199 payload: the payload object to check
200 bsdiff_in_place: whether to perform BSDIFF operation in-place (optional)
201
202 """
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800203 assert payload.is_init, 'uninitialized update payload'
204 self.payload = payload
205 self.block_size = payload.manifest.block_size
Gilad Arnold272a4992013-05-08 13:12:53 -0700206 self.bsdiff_in_place = bsdiff_in_place
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800207
208 def _ApplyReplaceOperation(self, op, op_name, out_data, part_file, part_size):
209 """Applies a REPLACE{,_BZ} operation.
210
211 Args:
212 op: the operation object
213 op_name: name string for error reporting
214 out_data: the data to be written
215 part_file: the partition file object
216 part_size: the size of the partition
217 Raises:
218 PayloadError if something goes wrong.
219
220 """
221 block_size = self.block_size
222 data_length = len(out_data)
223
224 # Decompress data if needed.
225 if op.type == common.OpType.REPLACE_BZ:
226 out_data = bz2.decompress(out_data)
227 data_length = len(out_data)
228
229 # Write data to blocks specified in dst extents.
230 data_start = 0
231 for ex, ex_name in common.ExtentIter(op.dst_extents,
232 '%s.dst_extents' % op_name):
233 start_block = ex.start_block
234 num_blocks = ex.num_blocks
235 count = num_blocks * block_size
236
237 # Make sure it's not a fake (signature) operation.
238 if start_block != common.PSEUDO_EXTENT_MARKER:
239 data_end = data_start + count
240
241 # Make sure we're not running past partition boundary.
242 if (start_block + num_blocks) * block_size > part_size:
243 raise PayloadError(
244 '%s: extent (%s) exceeds partition size (%d)' %
245 (ex_name, common.FormatExtent(ex, block_size),
246 part_size))
247
248 # Make sure that we have enough data to write.
249 if data_end >= data_length + block_size:
250 raise PayloadError(
251 '%s: more dst blocks than data (even with padding)')
252
253 # Pad with zeros if necessary.
254 if data_end > data_length:
255 padding = data_end - data_length
256 out_data += '\0' * padding
257
258 self.payload.payload_file.seek(start_block * block_size)
259 part_file.seek(start_block * block_size)
260 part_file.write(out_data[data_start:data_end])
261
262 data_start += count
263
264 # Make sure we wrote all data.
265 if data_start < data_length:
266 raise PayloadError('%s: wrote fewer bytes (%d) than expected (%d)' %
267 (op_name, data_start, data_length))
268
269 def _ApplyMoveOperation(self, op, op_name, part_file):
270 """Applies a MOVE operation.
271
Gilad Arnold658185a2013-05-08 17:57:54 -0700272 Note that this operation must read the whole block data from the input and
273 only then dump it, due to our in-place update semantics; otherwise, it
274 might clobber data midway through.
275
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800276 Args:
277 op: the operation object
278 op_name: name string for error reporting
279 part_file: the partition file object
280 Raises:
281 PayloadError if something goes wrong.
282
283 """
284 block_size = self.block_size
285
286 # Gather input raw data from src extents.
287 in_data = _ReadExtents(part_file, op.src_extents, block_size)
288
289 # Dump extracted data to dst extents.
290 _WriteExtents(part_file, in_data, op.dst_extents, block_size,
291 '%s.dst_extents' % op_name)
292
293 def _ApplyBsdiffOperation(self, op, op_name, patch_data, part_file):
294 """Applies a BSDIFF operation.
295
296 Args:
297 op: the operation object
298 op_name: name string for error reporting
299 patch_data: the binary patch content
300 part_file: the partition file object
301 Raises:
302 PayloadError if something goes wrong.
303
304 """
305 block_size = self.block_size
306
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800307 # Dump patch data to file.
308 with tempfile.NamedTemporaryFile(delete=False) as patch_file:
309 patch_file_name = patch_file.name
310 patch_file.write(patch_data)
311
Gilad Arnold272a4992013-05-08 13:12:53 -0700312 if self.bsdiff_in_place and hasattr(part_file, 'fileno'):
313 # Construct input and output extents argument for bspatch.
314 in_extents_arg, _, _ = _ExtentsToBspatchArg(
315 op.src_extents, block_size, '%s.src_extents' % op_name,
316 data_length=op.src_length)
317 out_extents_arg, pad_off, pad_len = _ExtentsToBspatchArg(
318 op.dst_extents, block_size, '%s.dst_extents' % op_name,
319 data_length=op.dst_length)
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800320
Gilad Arnold272a4992013-05-08 13:12:53 -0700321 # Invoke bspatch on partition file with extents args.
322 file_name = '/dev/fd/%d' % part_file.fileno()
323 bspatch_cmd = ['bspatch', file_name, file_name, patch_file_name,
324 in_extents_arg, out_extents_arg]
325 subprocess.check_call(bspatch_cmd)
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800326
Gilad Arnold272a4992013-05-08 13:12:53 -0700327 # Pad with zeros past the total output length.
328 if pad_len:
329 part_file.seek(pad_off)
330 part_file.write('\0' * pad_len)
331 else:
332 # Gather input raw data and write to a temp file.
333 in_data = _ReadExtents(part_file, op.src_extents, block_size,
334 max_length=op.src_length)
335 with tempfile.NamedTemporaryFile(delete=False) as in_file:
336 in_file_name = in_file.name
337 in_file.write(in_data)
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800338
Gilad Arnold272a4992013-05-08 13:12:53 -0700339 # Allocate tepmorary output file.
340 with tempfile.NamedTemporaryFile(delete=False) as out_file:
341 out_file_name = out_file.name
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800342
Gilad Arnold272a4992013-05-08 13:12:53 -0700343 # Invoke bspatch.
344 bspatch_cmd = ['bspatch', in_file_name, out_file_name, patch_file_name]
345 subprocess.check_call(bspatch_cmd)
346
347 # Read output.
348 with open(out_file_name, 'rb') as out_file:
349 out_data = out_file.read()
350 if len(out_data) != op.dst_length:
351 raise PayloadError(
352 '%s: actual patched data length (%d) not as expected (%d)' %
353 (op_name, len(out_data), op.dst_length))
354
355 # Write output back to partition, with padding.
356 unaligned_out_len = len(out_data) % block_size
357 if unaligned_out_len:
358 out_data += '\0' * (block_size - unaligned_out_len)
359 _WriteExtents(part_file, out_data, op.dst_extents, block_size,
360 '%s.dst_extents' % op_name)
361
362 # Delete input/output files.
363 os.remove(in_file_name)
364 os.remove(out_file_name)
365
366 # Delete patch file.
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800367 os.remove(patch_file_name)
368
369 def _ApplyOperations(self, operations, base_name, part_file, part_size):
370 """Applies a sequence of update operations to a partition.
371
372 This assumes an in-place update semantics, namely all reads are performed
373 first, then the data is processed and written back to the same file.
374
375 Args:
376 operations: the sequence of operations
377 base_name: the name of the operation sequence
378 part_file: the partition file object, open for reading/writing
379 part_size: the partition size
380 Raises:
381 PayloadError if anything goes wrong while processing the payload.
382
383 """
384 for op, op_name in common.OperationIter(operations, base_name):
385 # Read data blob.
386 data = self.payload.ReadDataBlob(op.data_offset, op.data_length)
387
388 if op.type in (common.OpType.REPLACE, common.OpType.REPLACE_BZ):
389 self._ApplyReplaceOperation(op, op_name, data, part_file, part_size)
390 elif op.type == common.OpType.MOVE:
391 self._ApplyMoveOperation(op, op_name, part_file)
392 elif op.type == common.OpType.BSDIFF:
393 self._ApplyBsdiffOperation(op, op_name, data, part_file)
394 else:
395 raise PayloadError('%s: unknown operation type (%d)' %
396 (op_name, op.type))
397
398 def _ApplyToPartition(self, operations, part_name, base_name,
Gilad Arnold16416602013-05-04 21:40:39 -0700399 new_part_file_name, new_part_info,
400 old_part_file_name=None, old_part_info=None):
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800401 """Applies an update to a partition.
402
403 Args:
404 operations: the sequence of update operations to apply
405 part_name: the name of the partition, for error reporting
406 base_name: the name of the operation sequence
Gilad Arnold16416602013-05-04 21:40:39 -0700407 new_part_file_name: file name to write partition data to
408 new_part_info: size and expected hash of dest partition
409 old_part_file_name: file name of source partition (optional)
410 old_part_info: size and expected hash of source partition (optional)
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800411 Raises:
412 PayloadError if anything goes wrong with the update.
413
414 """
415 # Do we have a source partition?
Gilad Arnold16416602013-05-04 21:40:39 -0700416 if old_part_file_name:
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800417 # Verify the source partition.
Gilad Arnold16416602013-05-04 21:40:39 -0700418 with open(old_part_file_name, 'rb') as old_part_file:
419 _VerifySha256(old_part_file, old_part_info.hash, part_name,
420 length=old_part_info.size)
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800421
422 # Copy the src partition to the dst one.
Gilad Arnold16416602013-05-04 21:40:39 -0700423 shutil.copyfile(old_part_file_name, new_part_file_name)
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800424 else:
425 # Preallocate the dst partition file.
426 subprocess.check_call(
Gilad Arnold16416602013-05-04 21:40:39 -0700427 ['fallocate', '-l', str(new_part_info.size), new_part_file_name])
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800428
429 # Apply operations.
Gilad Arnold16416602013-05-04 21:40:39 -0700430 with open(new_part_file_name, 'r+b') as new_part_file:
431 self._ApplyOperations(operations, base_name, new_part_file,
432 new_part_info.size)
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800433
434 # Verify the resulting partition.
Gilad Arnold16416602013-05-04 21:40:39 -0700435 with open(new_part_file_name, 'rb') as new_part_file:
436 _VerifySha256(new_part_file, new_part_info.hash, part_name,
437 length=new_part_info.size)
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800438
Gilad Arnold16416602013-05-04 21:40:39 -0700439 def Run(self, new_kernel_part, new_rootfs_part, old_kernel_part=None,
440 old_rootfs_part=None):
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800441 """Applier entry point, invoking all update operations.
442
443 Args:
Gilad Arnold16416602013-05-04 21:40:39 -0700444 new_kernel_part: name of dest kernel partition file
445 new_rootfs_part: name of dest rootfs partition file
446 old_kernel_part: name of source kernel partition file (optional)
447 old_rootfs_part: name of source rootfs partition file (optional)
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800448 Raises:
449 PayloadError if payload application failed.
450
451 """
452 self.payload.ResetFile()
453
454 # Make sure the arguments are sane and match the payload.
Gilad Arnold16416602013-05-04 21:40:39 -0700455 if not (new_kernel_part and new_rootfs_part):
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800456 raise PayloadError('missing dst {kernel,rootfs} partitions')
457
Gilad Arnold16416602013-05-04 21:40:39 -0700458 if not (old_kernel_part or old_rootfs_part):
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800459 if not self.payload.IsFull():
460 raise PayloadError('trying to apply a non-full update without src '
461 '{kernel,rootfs} partitions')
Gilad Arnold16416602013-05-04 21:40:39 -0700462 elif old_kernel_part and old_rootfs_part:
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800463 if not self.payload.IsDelta():
464 raise PayloadError('trying to apply a non-delta update onto src '
465 '{kernel,rootfs} partitions')
466 else:
467 raise PayloadError('not all src partitions provided')
468
469 # Apply update to rootfs.
470 self._ApplyToPartition(
471 self.payload.manifest.install_operations, 'rootfs',
Gilad Arnold16416602013-05-04 21:40:39 -0700472 'install_operations', new_rootfs_part,
473 self.payload.manifest.new_rootfs_info, old_rootfs_part,
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800474 self.payload.manifest.old_rootfs_info)
475
476 # Apply update to kernel update.
477 self._ApplyToPartition(
478 self.payload.manifest.kernel_install_operations, 'kernel',
Gilad Arnold16416602013-05-04 21:40:39 -0700479 'kernel_install_operations', new_kernel_part,
480 self.payload.manifest.new_kernel_info, old_kernel_part,
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800481 self.payload.manifest.old_kernel_info)