blob: 9a1b50905f60cfece0bbaa1dc4d2a91c3bda3955 [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
18import os
19import shutil
20import subprocess
21import sys
22import tempfile
23
24import common
25from error import PayloadError
26
27
28#
29# Helper functions.
30#
Gilad Arnold382df5c2013-05-03 12:49:28 -070031def _VerifySha256(file_obj, expected_hash, name, length=-1):
Gilad Arnold553b0ec2013-01-26 01:00:39 -080032 """Verifies the SHA256 hash of a file.
33
34 Args:
35 file_obj: file object to read
36 expected_hash: the hash digest we expect to be getting
37 name: name string of this hash, for error reporting
Gilad Arnold382df5c2013-05-03 12:49:28 -070038 length: precise length of data to verify (optional)
Gilad Arnold553b0ec2013-01-26 01:00:39 -080039 Raises:
Gilad Arnold382df5c2013-05-03 12:49:28 -070040 PayloadError if computed hash doesn't match expected one, or if fails to
41 read the specified length of data.
Gilad Arnold553b0ec2013-01-26 01:00:39 -080042
43 """
44 # pylint: disable=E1101
45 hasher = hashlib.sha256()
46 block_length = 1024 * 1024
Gilad Arnold382df5c2013-05-03 12:49:28 -070047 max_length = length if length >= 0 else sys.maxint
Gilad Arnold553b0ec2013-01-26 01:00:39 -080048
Gilad Arnold382df5c2013-05-03 12:49:28 -070049 while max_length > 0:
Gilad Arnold553b0ec2013-01-26 01:00:39 -080050 read_length = min(max_length, block_length)
51 data = file_obj.read(read_length)
52 if not data:
53 break
54 max_length -= len(data)
55 hasher.update(data)
56
Gilad Arnold382df5c2013-05-03 12:49:28 -070057 if length >= 0 and max_length > 0:
58 raise PayloadError(
59 'insufficient data (%d instead of %d) when verifying %s' %
60 (length - max_length, length, name))
61
Gilad Arnold553b0ec2013-01-26 01:00:39 -080062 actual_hash = hasher.digest()
63 if actual_hash != expected_hash:
64 raise PayloadError('%s hash (%s) not as expected (%s)' %
Gilad Arnold96405372013-05-04 00:24:58 -070065 (name, common.FormatSha256(actual_hash),
66 common.FormatSha256(expected_hash)))
Gilad Arnold553b0ec2013-01-26 01:00:39 -080067
68
69def _ReadExtents(file_obj, extents, block_size, max_length=-1):
70 """Reads data from file as defined by extent sequence.
71
72 This tries to be efficient by not copying data as it is read in chunks.
73
74 Args:
75 file_obj: file object
76 extents: sequence of block extents (offset and length)
77 block_size: size of each block
78 max_length: maximum length to read (optional)
79 Returns:
80 A character array containing the concatenated read data.
81
82 """
83 data = array.array('c')
Gilad Arnold272a4992013-05-08 13:12:53 -070084 if max_length < 0:
85 max_length = sys.maxint
Gilad Arnold553b0ec2013-01-26 01:00:39 -080086 for ex in extents:
87 if max_length == 0:
88 break
89 file_obj.seek(ex.start_block * block_size)
Gilad Arnold272a4992013-05-08 13:12:53 -070090 read_length = min(max_length, ex.num_blocks * block_size)
Gilad Arnold553b0ec2013-01-26 01:00:39 -080091 data.fromfile(file_obj, read_length)
Gilad Arnold272a4992013-05-08 13:12:53 -070092 max_length -= read_length
Gilad Arnold553b0ec2013-01-26 01:00:39 -080093 return data
94
95
96def _WriteExtents(file_obj, data, extents, block_size, base_name):
Gilad Arnold272a4992013-05-08 13:12:53 -070097 """Writes data to file as defined by extent sequence.
Gilad Arnold553b0ec2013-01-26 01:00:39 -080098
99 This tries to be efficient by not copy data as it is written in chunks.
100
101 Args:
102 file_obj: file object
103 data: data to write
104 extents: sequence of block extents (offset and length)
105 block_size: size of each block
Gilad Arnold272a4992013-05-08 13:12:53 -0700106 base_name: name string of extent sequence for error reporting
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800107 Raises:
108 PayloadError when things don't add up.
109
110 """
111 data_offset = 0
112 data_length = len(data)
113 for ex, ex_name in common.ExtentIter(extents, base_name):
Gilad Arnold272a4992013-05-08 13:12:53 -0700114 if not data_length:
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800115 raise PayloadError('%s: more write extents than data' % ex_name)
Gilad Arnold272a4992013-05-08 13:12:53 -0700116 write_length = min(data_length, ex.num_blocks * block_size)
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800117 file_obj.seek(ex.start_block * block_size)
118 data_view = buffer(data, data_offset, write_length)
119 file_obj.write(data_view)
120 data_offset += write_length
Gilad Arnold272a4992013-05-08 13:12:53 -0700121 data_length -= write_length
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800122
Gilad Arnold272a4992013-05-08 13:12:53 -0700123 if data_length:
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800124 raise PayloadError('%s: more data than write extents' % base_name)
125
126
Gilad Arnold272a4992013-05-08 13:12:53 -0700127def _ExtentsToBspatchArg(extents, block_size, base_name, data_length=-1):
128 """Translates an extent sequence into a bspatch-compatible string argument.
129
130 Args:
131 extents: sequence of block extents (offset and length)
132 block_size: size of each block
133 base_name: name string of extent sequence for error reporting
134 data_length: the actual total length of the data in bytes (optional)
135 Returns:
136 A tuple consisting of (i) a string of the form
137 "off_1:len_1,...,off_n:len_n", (ii) an offset where zero padding is needed
138 for filling the last extent, (iii) the length of the padding (zero means no
139 padding is needed and the extents cover the full length of data).
140 Raises:
141 PayloadError if data_length is too short or too long.
142
143 """
144 arg = ''
145 pad_off = pad_len = 0
146 if data_length < 0:
147 data_length = sys.maxint
148 for ex, ex_name in common.ExtentIter(extents, base_name):
149 if not data_length:
150 raise PayloadError('%s: more extents than total data length' % ex_name)
151 start_byte = ex.start_block * block_size
152 num_bytes = ex.num_blocks * block_size
153 if data_length < num_bytes:
154 pad_off = start_byte + data_length
155 pad_len = num_bytes - data_length
156 num_bytes = data_length
157 arg += '%s%d:%d' % (arg and ',', start_byte, num_bytes)
158 data_length -= num_bytes
159
160 if data_length:
161 raise PayloadError('%s: extents not covering full data length' % base_name)
162
163 return arg, pad_off, pad_len
164
165
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800166#
167# Payload application.
168#
169class PayloadApplier(object):
170 """Applying an update payload.
171
172 This is a short-lived object whose purpose is to isolate the logic used for
173 applying an update payload.
174
175 """
176
Gilad Arnold272a4992013-05-08 13:12:53 -0700177 def __init__(self, payload, bsdiff_in_place=True):
178 """Initialize the applier.
179
180 Args:
181 payload: the payload object to check
182 bsdiff_in_place: whether to perform BSDIFF operation in-place (optional)
183
184 """
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800185 assert payload.is_init, 'uninitialized update payload'
186 self.payload = payload
187 self.block_size = payload.manifest.block_size
Gilad Arnold272a4992013-05-08 13:12:53 -0700188 self.bsdiff_in_place = bsdiff_in_place
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800189
190 def _ApplyReplaceOperation(self, op, op_name, out_data, part_file, part_size):
191 """Applies a REPLACE{,_BZ} operation.
192
193 Args:
194 op: the operation object
195 op_name: name string for error reporting
196 out_data: the data to be written
197 part_file: the partition file object
198 part_size: the size of the partition
199 Raises:
200 PayloadError if something goes wrong.
201
202 """
203 block_size = self.block_size
204 data_length = len(out_data)
205
206 # Decompress data if needed.
207 if op.type == common.OpType.REPLACE_BZ:
208 out_data = bz2.decompress(out_data)
209 data_length = len(out_data)
210
211 # Write data to blocks specified in dst extents.
212 data_start = 0
213 for ex, ex_name in common.ExtentIter(op.dst_extents,
214 '%s.dst_extents' % op_name):
215 start_block = ex.start_block
216 num_blocks = ex.num_blocks
217 count = num_blocks * block_size
218
219 # Make sure it's not a fake (signature) operation.
220 if start_block != common.PSEUDO_EXTENT_MARKER:
221 data_end = data_start + count
222
223 # Make sure we're not running past partition boundary.
224 if (start_block + num_blocks) * block_size > part_size:
225 raise PayloadError(
226 '%s: extent (%s) exceeds partition size (%d)' %
227 (ex_name, common.FormatExtent(ex, block_size),
228 part_size))
229
230 # Make sure that we have enough data to write.
231 if data_end >= data_length + block_size:
232 raise PayloadError(
233 '%s: more dst blocks than data (even with padding)')
234
235 # Pad with zeros if necessary.
236 if data_end > data_length:
237 padding = data_end - data_length
238 out_data += '\0' * padding
239
240 self.payload.payload_file.seek(start_block * block_size)
241 part_file.seek(start_block * block_size)
242 part_file.write(out_data[data_start:data_end])
243
244 data_start += count
245
246 # Make sure we wrote all data.
247 if data_start < data_length:
248 raise PayloadError('%s: wrote fewer bytes (%d) than expected (%d)' %
249 (op_name, data_start, data_length))
250
251 def _ApplyMoveOperation(self, op, op_name, part_file):
252 """Applies a MOVE operation.
253
254 Args:
255 op: the operation object
256 op_name: name string for error reporting
257 part_file: the partition file object
258 Raises:
259 PayloadError if something goes wrong.
260
261 """
262 block_size = self.block_size
263
264 # Gather input raw data from src extents.
265 in_data = _ReadExtents(part_file, op.src_extents, block_size)
266
267 # Dump extracted data to dst extents.
268 _WriteExtents(part_file, in_data, op.dst_extents, block_size,
269 '%s.dst_extents' % op_name)
270
271 def _ApplyBsdiffOperation(self, op, op_name, patch_data, part_file):
272 """Applies a BSDIFF operation.
273
274 Args:
275 op: the operation object
276 op_name: name string for error reporting
277 patch_data: the binary patch content
278 part_file: the partition file object
279 Raises:
280 PayloadError if something goes wrong.
281
282 """
283 block_size = self.block_size
284
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800285 # Dump patch data to file.
286 with tempfile.NamedTemporaryFile(delete=False) as patch_file:
287 patch_file_name = patch_file.name
288 patch_file.write(patch_data)
289
Gilad Arnold272a4992013-05-08 13:12:53 -0700290 if self.bsdiff_in_place and hasattr(part_file, 'fileno'):
291 # Construct input and output extents argument for bspatch.
292 in_extents_arg, _, _ = _ExtentsToBspatchArg(
293 op.src_extents, block_size, '%s.src_extents' % op_name,
294 data_length=op.src_length)
295 out_extents_arg, pad_off, pad_len = _ExtentsToBspatchArg(
296 op.dst_extents, block_size, '%s.dst_extents' % op_name,
297 data_length=op.dst_length)
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800298
Gilad Arnold272a4992013-05-08 13:12:53 -0700299 # Invoke bspatch on partition file with extents args.
300 file_name = '/dev/fd/%d' % part_file.fileno()
301 bspatch_cmd = ['bspatch', file_name, file_name, patch_file_name,
302 in_extents_arg, out_extents_arg]
303 subprocess.check_call(bspatch_cmd)
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800304
Gilad Arnold272a4992013-05-08 13:12:53 -0700305 # Pad with zeros past the total output length.
306 if pad_len:
307 part_file.seek(pad_off)
308 part_file.write('\0' * pad_len)
309 else:
310 # Gather input raw data and write to a temp file.
311 in_data = _ReadExtents(part_file, op.src_extents, block_size,
312 max_length=op.src_length)
313 with tempfile.NamedTemporaryFile(delete=False) as in_file:
314 in_file_name = in_file.name
315 in_file.write(in_data)
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800316
Gilad Arnold272a4992013-05-08 13:12:53 -0700317 # Allocate tepmorary output file.
318 with tempfile.NamedTemporaryFile(delete=False) as out_file:
319 out_file_name = out_file.name
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800320
Gilad Arnold272a4992013-05-08 13:12:53 -0700321 # Invoke bspatch.
322 bspatch_cmd = ['bspatch', in_file_name, out_file_name, patch_file_name]
323 subprocess.check_call(bspatch_cmd)
324
325 # Read output.
326 with open(out_file_name, 'rb') as out_file:
327 out_data = out_file.read()
328 if len(out_data) != op.dst_length:
329 raise PayloadError(
330 '%s: actual patched data length (%d) not as expected (%d)' %
331 (op_name, len(out_data), op.dst_length))
332
333 # Write output back to partition, with padding.
334 unaligned_out_len = len(out_data) % block_size
335 if unaligned_out_len:
336 out_data += '\0' * (block_size - unaligned_out_len)
337 _WriteExtents(part_file, out_data, op.dst_extents, block_size,
338 '%s.dst_extents' % op_name)
339
340 # Delete input/output files.
341 os.remove(in_file_name)
342 os.remove(out_file_name)
343
344 # Delete patch file.
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800345 os.remove(patch_file_name)
346
347 def _ApplyOperations(self, operations, base_name, part_file, part_size):
348 """Applies a sequence of update operations to a partition.
349
350 This assumes an in-place update semantics, namely all reads are performed
351 first, then the data is processed and written back to the same file.
352
353 Args:
354 operations: the sequence of operations
355 base_name: the name of the operation sequence
356 part_file: the partition file object, open for reading/writing
357 part_size: the partition size
358 Raises:
359 PayloadError if anything goes wrong while processing the payload.
360
361 """
362 for op, op_name in common.OperationIter(operations, base_name):
363 # Read data blob.
364 data = self.payload.ReadDataBlob(op.data_offset, op.data_length)
365
366 if op.type in (common.OpType.REPLACE, common.OpType.REPLACE_BZ):
367 self._ApplyReplaceOperation(op, op_name, data, part_file, part_size)
368 elif op.type == common.OpType.MOVE:
369 self._ApplyMoveOperation(op, op_name, part_file)
370 elif op.type == common.OpType.BSDIFF:
371 self._ApplyBsdiffOperation(op, op_name, data, part_file)
372 else:
373 raise PayloadError('%s: unknown operation type (%d)' %
374 (op_name, op.type))
375
376 def _ApplyToPartition(self, operations, part_name, base_name,
Gilad Arnold16416602013-05-04 21:40:39 -0700377 new_part_file_name, new_part_info,
378 old_part_file_name=None, old_part_info=None):
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800379 """Applies an update to a partition.
380
381 Args:
382 operations: the sequence of update operations to apply
383 part_name: the name of the partition, for error reporting
384 base_name: the name of the operation sequence
Gilad Arnold16416602013-05-04 21:40:39 -0700385 new_part_file_name: file name to write partition data to
386 new_part_info: size and expected hash of dest partition
387 old_part_file_name: file name of source partition (optional)
388 old_part_info: size and expected hash of source partition (optional)
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800389 Raises:
390 PayloadError if anything goes wrong with the update.
391
392 """
393 # Do we have a source partition?
Gilad Arnold16416602013-05-04 21:40:39 -0700394 if old_part_file_name:
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800395 # Verify the source partition.
Gilad Arnold16416602013-05-04 21:40:39 -0700396 with open(old_part_file_name, 'rb') as old_part_file:
397 _VerifySha256(old_part_file, old_part_info.hash, part_name,
398 length=old_part_info.size)
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800399
400 # Copy the src partition to the dst one.
Gilad Arnold16416602013-05-04 21:40:39 -0700401 shutil.copyfile(old_part_file_name, new_part_file_name)
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800402 else:
403 # Preallocate the dst partition file.
404 subprocess.check_call(
Gilad Arnold16416602013-05-04 21:40:39 -0700405 ['fallocate', '-l', str(new_part_info.size), new_part_file_name])
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800406
407 # Apply operations.
Gilad Arnold16416602013-05-04 21:40:39 -0700408 with open(new_part_file_name, 'r+b') as new_part_file:
409 self._ApplyOperations(operations, base_name, new_part_file,
410 new_part_info.size)
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800411
412 # Verify the resulting partition.
Gilad Arnold16416602013-05-04 21:40:39 -0700413 with open(new_part_file_name, 'rb') as new_part_file:
414 _VerifySha256(new_part_file, new_part_info.hash, part_name,
415 length=new_part_info.size)
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800416
Gilad Arnold16416602013-05-04 21:40:39 -0700417 def Run(self, new_kernel_part, new_rootfs_part, old_kernel_part=None,
418 old_rootfs_part=None):
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800419 """Applier entry point, invoking all update operations.
420
421 Args:
Gilad Arnold16416602013-05-04 21:40:39 -0700422 new_kernel_part: name of dest kernel partition file
423 new_rootfs_part: name of dest rootfs partition file
424 old_kernel_part: name of source kernel partition file (optional)
425 old_rootfs_part: name of source rootfs partition file (optional)
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800426 Raises:
427 PayloadError if payload application failed.
428
429 """
430 self.payload.ResetFile()
431
432 # Make sure the arguments are sane and match the payload.
Gilad Arnold16416602013-05-04 21:40:39 -0700433 if not (new_kernel_part and new_rootfs_part):
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800434 raise PayloadError('missing dst {kernel,rootfs} partitions')
435
Gilad Arnold16416602013-05-04 21:40:39 -0700436 if not (old_kernel_part or old_rootfs_part):
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800437 if not self.payload.IsFull():
438 raise PayloadError('trying to apply a non-full update without src '
439 '{kernel,rootfs} partitions')
Gilad Arnold16416602013-05-04 21:40:39 -0700440 elif old_kernel_part and old_rootfs_part:
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800441 if not self.payload.IsDelta():
442 raise PayloadError('trying to apply a non-delta update onto src '
443 '{kernel,rootfs} partitions')
444 else:
445 raise PayloadError('not all src partitions provided')
446
447 # Apply update to rootfs.
448 self._ApplyToPartition(
449 self.payload.manifest.install_operations, 'rootfs',
Gilad Arnold16416602013-05-04 21:40:39 -0700450 'install_operations', new_rootfs_part,
451 self.payload.manifest.new_rootfs_info, old_rootfs_part,
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800452 self.payload.manifest.old_rootfs_info)
453
454 # Apply update to kernel update.
455 self._ApplyToPartition(
456 self.payload.manifest.kernel_install_operations, 'kernel',
Gilad Arnold16416602013-05-04 21:40:39 -0700457 'kernel_install_operations', new_kernel_part,
458 self.payload.manifest.new_kernel_info, old_kernel_part,
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800459 self.payload.manifest.old_kernel_info)