• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# 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
15from __future__ import print_function
16
17import array
18import bz2
19import hashlib
20import itertools
21import os
22import shutil
23import subprocess
24import sys
25import tempfile
26
27from update_payload import common
28from update_payload.error import PayloadError
29
30
31#
32# Helper functions.
33#
34def _VerifySha256(file_obj, expected_hash, name, length=-1):
35  """Verifies the SHA256 hash of a file.
36
37  Args:
38    file_obj: file object to read
39    expected_hash: the hash digest we expect to be getting
40    name: name string of this hash, for error reporting
41    length: precise length of data to verify (optional)
42
43  Raises:
44    PayloadError if computed hash doesn't match expected one, or if fails to
45    read the specified length of data.
46  """
47  hasher = hashlib.sha256()
48  block_length = 1024 * 1024
49  max_length = length if length >= 0 else sys.maxint
50
51  while max_length > 0:
52    read_length = min(max_length, block_length)
53    data = file_obj.read(read_length)
54    if not data:
55      break
56    max_length -= len(data)
57    hasher.update(data)
58
59  if length >= 0 and max_length > 0:
60    raise PayloadError(
61        'insufficient data (%d instead of %d) when verifying %s' %
62        (length - max_length, length, name))
63
64  actual_hash = hasher.digest()
65  if actual_hash != expected_hash:
66    raise PayloadError('%s hash (%s) not as expected (%s)' %
67                       (name, common.FormatSha256(actual_hash),
68                        common.FormatSha256(expected_hash)))
69
70
71def _ReadExtents(file_obj, extents, block_size, max_length=-1):
72  """Reads data from file as defined by extent sequence.
73
74  This tries to be efficient by not copying data as it is read in chunks.
75
76  Args:
77    file_obj: file object
78    extents: sequence of block extents (offset and length)
79    block_size: size of each block
80    max_length: maximum length to read (optional)
81
82  Returns:
83    A character array containing the concatenated read data.
84  """
85  data = array.array('c')
86  if max_length < 0:
87    max_length = sys.maxint
88  for ex in extents:
89    if max_length == 0:
90      break
91    read_length = min(max_length, ex.num_blocks * block_size)
92
93    # Fill with zeros or read from file, depending on the type of extent.
94    if ex.start_block == common.PSEUDO_EXTENT_MARKER:
95      data.extend(itertools.repeat('\0', read_length))
96    else:
97      file_obj.seek(ex.start_block * block_size)
98      data.fromfile(file_obj, read_length)
99
100    max_length -= read_length
101
102  return data
103
104
105def _WriteExtents(file_obj, data, extents, block_size, base_name):
106  """Writes data to file as defined by extent sequence.
107
108  This tries to be efficient by not copy data as it is written in chunks.
109
110  Args:
111    file_obj: file object
112    data: data to write
113    extents: sequence of block extents (offset and length)
114    block_size: size of each block
115    base_name: name string of extent sequence for error reporting
116
117  Raises:
118    PayloadError when things don't add up.
119  """
120  data_offset = 0
121  data_length = len(data)
122  for ex, ex_name in common.ExtentIter(extents, base_name):
123    if not data_length:
124      raise PayloadError('%s: more write extents than data' % ex_name)
125    write_length = min(data_length, ex.num_blocks * block_size)
126
127    # Only do actual writing if this is not a pseudo-extent.
128    if ex.start_block != common.PSEUDO_EXTENT_MARKER:
129      file_obj.seek(ex.start_block * block_size)
130      data_view = buffer(data, data_offset, write_length)
131      file_obj.write(data_view)
132
133    data_offset += write_length
134    data_length -= write_length
135
136  if data_length:
137    raise PayloadError('%s: more data than write extents' % base_name)
138
139
140def _ExtentsToBspatchArg(extents, block_size, base_name, data_length=-1):
141  """Translates an extent sequence into a bspatch-compatible string argument.
142
143  Args:
144    extents: sequence of block extents (offset and length)
145    block_size: size of each block
146    base_name: name string of extent sequence for error reporting
147    data_length: the actual total length of the data in bytes (optional)
148
149  Returns:
150    A tuple consisting of (i) a string of the form
151    "off_1:len_1,...,off_n:len_n", (ii) an offset where zero padding is needed
152    for filling the last extent, (iii) the length of the padding (zero means no
153    padding is needed and the extents cover the full length of data).
154
155  Raises:
156    PayloadError if data_length is too short or too long.
157  """
158  arg = ''
159  pad_off = pad_len = 0
160  if data_length < 0:
161    data_length = sys.maxint
162  for ex, ex_name in common.ExtentIter(extents, base_name):
163    if not data_length:
164      raise PayloadError('%s: more extents than total data length' % ex_name)
165
166    is_pseudo = ex.start_block == common.PSEUDO_EXTENT_MARKER
167    start_byte = -1 if is_pseudo else ex.start_block * block_size
168    num_bytes = ex.num_blocks * block_size
169    if data_length < num_bytes:
170      # We're only padding a real extent.
171      if not is_pseudo:
172        pad_off = start_byte + data_length
173        pad_len = num_bytes - data_length
174
175      num_bytes = data_length
176
177    arg += '%s%d:%d' % (arg and ',', start_byte, num_bytes)
178    data_length -= num_bytes
179
180  if data_length:
181    raise PayloadError('%s: extents not covering full data length' % base_name)
182
183  return arg, pad_off, pad_len
184
185
186#
187# Payload application.
188#
189class PayloadApplier(object):
190  """Applying an update payload.
191
192  This is a short-lived object whose purpose is to isolate the logic used for
193  applying an update payload.
194  """
195
196  def __init__(self, payload, bsdiff_in_place=True, bspatch_path=None,
197               puffpatch_path=None, truncate_to_expected_size=True):
198    """Initialize the applier.
199
200    Args:
201      payload: the payload object to check
202      bsdiff_in_place: whether to perform BSDIFF operation in-place (optional)
203      bspatch_path: path to the bspatch binary (optional)
204      puffpatch_path: path to the puffpatch binary (optional)
205      truncate_to_expected_size: whether to truncate the resulting partitions
206                                 to their expected sizes, as specified in the
207                                 payload (optional)
208    """
209    assert payload.is_init, 'uninitialized update payload'
210    self.payload = payload
211    self.block_size = payload.manifest.block_size
212    self.minor_version = payload.manifest.minor_version
213    self.bsdiff_in_place = bsdiff_in_place
214    self.bspatch_path = bspatch_path or 'bspatch'
215    self.puffpatch_path = puffpatch_path or 'puffin'
216    self.truncate_to_expected_size = truncate_to_expected_size
217
218  def _ApplyReplaceOperation(self, op, op_name, out_data, part_file, part_size):
219    """Applies a REPLACE{,_BZ} operation.
220
221    Args:
222      op: the operation object
223      op_name: name string for error reporting
224      out_data: the data to be written
225      part_file: the partition file object
226      part_size: the size of the partition
227
228    Raises:
229      PayloadError if something goes wrong.
230    """
231    block_size = self.block_size
232    data_length = len(out_data)
233
234    # Decompress data if needed.
235    if op.type == common.OpType.REPLACE_BZ:
236      out_data = bz2.decompress(out_data)
237      data_length = len(out_data)
238
239    # Write data to blocks specified in dst extents.
240    data_start = 0
241    for ex, ex_name in common.ExtentIter(op.dst_extents,
242                                         '%s.dst_extents' % op_name):
243      start_block = ex.start_block
244      num_blocks = ex.num_blocks
245      count = num_blocks * block_size
246
247      # Make sure it's not a fake (signature) operation.
248      if start_block != common.PSEUDO_EXTENT_MARKER:
249        data_end = data_start + count
250
251        # Make sure we're not running past partition boundary.
252        if (start_block + num_blocks) * block_size > part_size:
253          raise PayloadError(
254              '%s: extent (%s) exceeds partition size (%d)' %
255              (ex_name, common.FormatExtent(ex, block_size),
256               part_size))
257
258        # Make sure that we have enough data to write.
259        if data_end >= data_length + block_size:
260          raise PayloadError(
261              '%s: more dst blocks than data (even with padding)')
262
263        # Pad with zeros if necessary.
264        if data_end > data_length:
265          padding = data_end - data_length
266          out_data += '\0' * padding
267
268        self.payload.payload_file.seek(start_block * block_size)
269        part_file.seek(start_block * block_size)
270        part_file.write(out_data[data_start:data_end])
271
272      data_start += count
273
274    # Make sure we wrote all data.
275    if data_start < data_length:
276      raise PayloadError('%s: wrote fewer bytes (%d) than expected (%d)' %
277                         (op_name, data_start, data_length))
278
279  def _ApplyMoveOperation(self, op, op_name, part_file):
280    """Applies a MOVE operation.
281
282    Note that this operation must read the whole block data from the input and
283    only then dump it, due to our in-place update semantics; otherwise, it
284    might clobber data midway through.
285
286    Args:
287      op: the operation object
288      op_name: name string for error reporting
289      part_file: the partition file object
290
291    Raises:
292      PayloadError if something goes wrong.
293    """
294    block_size = self.block_size
295
296    # Gather input raw data from src extents.
297    in_data = _ReadExtents(part_file, op.src_extents, block_size)
298
299    # Dump extracted data to dst extents.
300    _WriteExtents(part_file, in_data, op.dst_extents, block_size,
301                  '%s.dst_extents' % op_name)
302
303  def _ApplyZeroOperation(self, op, op_name, part_file):
304    """Applies a ZERO operation.
305
306    Args:
307      op: the operation object
308      op_name: name string for error reporting
309      part_file: the partition file object
310
311    Raises:
312      PayloadError if something goes wrong.
313    """
314    block_size = self.block_size
315    base_name = '%s.dst_extents' % op_name
316
317    # Iterate over the extents and write zero.
318    # pylint: disable=unused-variable
319    for ex, ex_name in common.ExtentIter(op.dst_extents, base_name):
320      # Only do actual writing if this is not a pseudo-extent.
321      if ex.start_block != common.PSEUDO_EXTENT_MARKER:
322        part_file.seek(ex.start_block * block_size)
323        part_file.write('\0' * (ex.num_blocks * block_size))
324
325  def _ApplySourceCopyOperation(self, op, op_name, old_part_file,
326                                new_part_file):
327    """Applies a SOURCE_COPY operation.
328
329    Args:
330      op: the operation object
331      op_name: name string for error reporting
332      old_part_file: the old partition file object
333      new_part_file: the new partition file object
334
335    Raises:
336      PayloadError if something goes wrong.
337    """
338    if not old_part_file:
339      raise PayloadError(
340          '%s: no source partition file provided for operation type (%d)' %
341          (op_name, op.type))
342
343    block_size = self.block_size
344
345    # Gather input raw data from src extents.
346    in_data = _ReadExtents(old_part_file, op.src_extents, block_size)
347
348    # Dump extracted data to dst extents.
349    _WriteExtents(new_part_file, in_data, op.dst_extents, block_size,
350                  '%s.dst_extents' % op_name)
351
352  def _BytesInExtents(self, extents, base_name):
353    """Counts the length of extents in bytes.
354
355    Args:
356      extents: The list of Extents.
357      base_name: For error reporting.
358
359    Returns:
360      The number of bytes in extents.
361    """
362
363    length = 0
364    # pylint: disable=unused-variable
365    for ex, ex_name in common.ExtentIter(extents, base_name):
366      length += ex.num_blocks * self.block_size
367    return length
368
369  def _ApplyDiffOperation(self, op, op_name, patch_data, old_part_file,
370                          new_part_file):
371    """Applies a SOURCE_BSDIFF, BROTLI_BSDIFF or PUFFDIFF operation.
372
373    Args:
374      op: the operation object
375      op_name: name string for error reporting
376      patch_data: the binary patch content
377      old_part_file: the source partition file object
378      new_part_file: the target partition file object
379
380    Raises:
381      PayloadError if something goes wrong.
382    """
383    if not old_part_file:
384      raise PayloadError(
385          '%s: no source partition file provided for operation type (%d)' %
386          (op_name, op.type))
387
388    block_size = self.block_size
389
390    # Dump patch data to file.
391    with tempfile.NamedTemporaryFile(delete=False) as patch_file:
392      patch_file_name = patch_file.name
393      patch_file.write(patch_data)
394
395    if (hasattr(new_part_file, 'fileno') and
396        ((not old_part_file) or hasattr(old_part_file, 'fileno'))):
397      # Construct input and output extents argument for bspatch.
398
399      in_extents_arg, _, _ = _ExtentsToBspatchArg(
400          op.src_extents, block_size, '%s.src_extents' % op_name,
401          data_length=op.src_length if op.src_length else
402          self._BytesInExtents(op.src_extents, "%s.src_extents"))
403      out_extents_arg, pad_off, pad_len = _ExtentsToBspatchArg(
404          op.dst_extents, block_size, '%s.dst_extents' % op_name,
405          data_length=op.dst_length if op.dst_length else
406          self._BytesInExtents(op.dst_extents, "%s.dst_extents"))
407
408      new_file_name = '/dev/fd/%d' % new_part_file.fileno()
409      # Diff from source partition.
410      old_file_name = '/dev/fd/%d' % old_part_file.fileno()
411
412      if op.type in (common.OpType.BSDIFF, common.OpType.SOURCE_BSDIFF,
413                     common.OpType.BROTLI_BSDIFF):
414        # Invoke bspatch on partition file with extents args.
415        bspatch_cmd = [self.bspatch_path, old_file_name, new_file_name,
416                       patch_file_name, in_extents_arg, out_extents_arg]
417        subprocess.check_call(bspatch_cmd)
418      elif op.type == common.OpType.PUFFDIFF:
419        # Invoke puffpatch on partition file with extents args.
420        puffpatch_cmd = [self.puffpatch_path,
421                         "--operation=puffpatch",
422                         "--src_file=%s" % old_file_name,
423                         "--dst_file=%s" % new_file_name,
424                         "--patch_file=%s" % patch_file_name,
425                         "--src_extents=%s" % in_extents_arg,
426                         "--dst_extents=%s" % out_extents_arg]
427        subprocess.check_call(puffpatch_cmd)
428      else:
429        raise PayloadError("Unknown operation %s", op.type)
430
431      # Pad with zeros past the total output length.
432      if pad_len:
433        new_part_file.seek(pad_off)
434        new_part_file.write('\0' * pad_len)
435    else:
436      # Gather input raw data and write to a temp file.
437      input_part_file = old_part_file if old_part_file else new_part_file
438      in_data = _ReadExtents(input_part_file, op.src_extents, block_size,
439                             max_length=op.src_length if op.src_length else
440                             self._BytesInExtents(op.src_extents,
441                                                  "%s.src_extents"))
442      with tempfile.NamedTemporaryFile(delete=False) as in_file:
443        in_file_name = in_file.name
444        in_file.write(in_data)
445
446      # Allocate temporary output file.
447      with tempfile.NamedTemporaryFile(delete=False) as out_file:
448        out_file_name = out_file.name
449
450      if op.type in (common.OpType.BSDIFF, common.OpType.SOURCE_BSDIFF,
451                     common.OpType.BROTLI_BSDIFF):
452        # Invoke bspatch.
453        bspatch_cmd = [self.bspatch_path, in_file_name, out_file_name,
454                       patch_file_name]
455        subprocess.check_call(bspatch_cmd)
456      elif op.type == common.OpType.PUFFDIFF:
457        # Invoke puffpatch.
458        puffpatch_cmd = [self.puffpatch_path,
459                         "--operation=puffpatch",
460                         "--src_file=%s" % in_file_name,
461                         "--dst_file=%s" % out_file_name,
462                         "--patch_file=%s" % patch_file_name]
463        subprocess.check_call(puffpatch_cmd)
464      else:
465        raise PayloadError("Unknown operation %s", op.type)
466
467      # Read output.
468      with open(out_file_name, 'rb') as out_file:
469        out_data = out_file.read()
470        if len(out_data) != op.dst_length:
471          raise PayloadError(
472              '%s: actual patched data length (%d) not as expected (%d)' %
473              (op_name, len(out_data), op.dst_length))
474
475      # Write output back to partition, with padding.
476      unaligned_out_len = len(out_data) % block_size
477      if unaligned_out_len:
478        out_data += '\0' * (block_size - unaligned_out_len)
479      _WriteExtents(new_part_file, out_data, op.dst_extents, block_size,
480                    '%s.dst_extents' % op_name)
481
482      # Delete input/output files.
483      os.remove(in_file_name)
484      os.remove(out_file_name)
485
486    # Delete patch file.
487    os.remove(patch_file_name)
488
489  def _ApplyOperations(self, operations, base_name, old_part_file,
490                       new_part_file, part_size):
491    """Applies a sequence of update operations to a partition.
492
493    This assumes an in-place update semantics for MOVE and BSDIFF, namely all
494    reads are performed first, then the data is processed and written back to
495    the same file.
496
497    Args:
498      operations: the sequence of operations
499      base_name: the name of the operation sequence
500      old_part_file: the old partition file object, open for reading/writing
501      new_part_file: the new partition file object, open for reading/writing
502      part_size: the partition size
503
504    Raises:
505      PayloadError if anything goes wrong while processing the payload.
506    """
507    for op, op_name in common.OperationIter(operations, base_name):
508      # Read data blob.
509      data = self.payload.ReadDataBlob(op.data_offset, op.data_length)
510
511      if op.type in (common.OpType.REPLACE, common.OpType.REPLACE_BZ):
512        self._ApplyReplaceOperation(op, op_name, data, new_part_file, part_size)
513      elif op.type == common.OpType.MOVE:
514        self._ApplyMoveOperation(op, op_name, new_part_file)
515      elif op.type == common.OpType.ZERO:
516        self._ApplyZeroOperation(op, op_name, new_part_file)
517      elif op.type == common.OpType.BSDIFF:
518        self._ApplyDiffOperation(op, op_name, data, new_part_file,
519                                 new_part_file)
520      elif op.type == common.OpType.SOURCE_COPY:
521        self._ApplySourceCopyOperation(op, op_name, old_part_file,
522                                       new_part_file)
523      elif op.type in (common.OpType.SOURCE_BSDIFF, common.OpType.PUFFDIFF,
524                       common.OpType.BROTLI_BSDIFF):
525        self._ApplyDiffOperation(op, op_name, data, old_part_file,
526                                 new_part_file)
527      else:
528        raise PayloadError('%s: unknown operation type (%d)' %
529                           (op_name, op.type))
530
531  def _ApplyToPartition(self, operations, part_name, base_name,
532                        new_part_file_name, new_part_info,
533                        old_part_file_name=None, old_part_info=None):
534    """Applies an update to a partition.
535
536    Args:
537      operations: the sequence of update operations to apply
538      part_name: the name of the partition, for error reporting
539      base_name: the name of the operation sequence
540      new_part_file_name: file name to write partition data to
541      new_part_info: size and expected hash of dest partition
542      old_part_file_name: file name of source partition (optional)
543      old_part_info: size and expected hash of source partition (optional)
544
545    Raises:
546      PayloadError if anything goes wrong with the update.
547    """
548    # Do we have a source partition?
549    if old_part_file_name:
550      # Verify the source partition.
551      with open(old_part_file_name, 'rb') as old_part_file:
552        _VerifySha256(old_part_file, old_part_info.hash,
553                      'old ' + part_name, length=old_part_info.size)
554      new_part_file_mode = 'r+b'
555      if self.minor_version == common.INPLACE_MINOR_PAYLOAD_VERSION:
556        # Copy the src partition to the dst one; make sure we don't truncate it.
557        shutil.copyfile(old_part_file_name, new_part_file_name)
558      elif (self.minor_version == common.SOURCE_MINOR_PAYLOAD_VERSION or
559            self.minor_version == common.OPSRCHASH_MINOR_PAYLOAD_VERSION or
560            self.minor_version == common.BROTLI_BSDIFF_MINOR_PAYLOAD_VERSION or
561            self.minor_version == common.PUFFDIFF_MINOR_PAYLOAD_VERSION):
562        # In minor version >= 2, we don't want to copy the partitions, so
563        # instead just make the new partition file.
564        open(new_part_file_name, 'w').close()
565      else:
566        raise PayloadError("Unknown minor version: %d" % self.minor_version)
567    else:
568      # We need to create/truncate the dst partition file.
569      new_part_file_mode = 'w+b'
570
571    # Apply operations.
572    with open(new_part_file_name, new_part_file_mode) as new_part_file:
573      old_part_file = (open(old_part_file_name, 'r+b')
574                       if old_part_file_name else None)
575      try:
576        self._ApplyOperations(operations, base_name, old_part_file,
577                              new_part_file, new_part_info.size)
578      finally:
579        if old_part_file:
580          old_part_file.close()
581
582      # Truncate the result, if so instructed.
583      if self.truncate_to_expected_size:
584        new_part_file.seek(0, 2)
585        if new_part_file.tell() > new_part_info.size:
586          new_part_file.seek(new_part_info.size)
587          new_part_file.truncate()
588
589    # Verify the resulting partition.
590    with open(new_part_file_name, 'rb') as new_part_file:
591      _VerifySha256(new_part_file, new_part_info.hash,
592                    'new ' + part_name, length=new_part_info.size)
593
594  def Run(self, new_kernel_part, new_rootfs_part, old_kernel_part=None,
595          old_rootfs_part=None):
596    """Applier entry point, invoking all update operations.
597
598    Args:
599      new_kernel_part: name of dest kernel partition file
600      new_rootfs_part: name of dest rootfs partition file
601      old_kernel_part: name of source kernel partition file (optional)
602      old_rootfs_part: name of source rootfs partition file (optional)
603
604    Raises:
605      PayloadError if payload application failed.
606    """
607    self.payload.ResetFile()
608
609    # Make sure the arguments are sane and match the payload.
610    if not (new_kernel_part and new_rootfs_part):
611      raise PayloadError('missing dst {kernel,rootfs} partitions')
612
613    if not (old_kernel_part or old_rootfs_part):
614      if not self.payload.IsFull():
615        raise PayloadError('trying to apply a non-full update without src '
616                           '{kernel,rootfs} partitions')
617    elif old_kernel_part and old_rootfs_part:
618      if not self.payload.IsDelta():
619        raise PayloadError('trying to apply a non-delta update onto src '
620                           '{kernel,rootfs} partitions')
621    else:
622      raise PayloadError('not all src partitions provided')
623
624    # Apply update to rootfs.
625    self._ApplyToPartition(
626        self.payload.manifest.install_operations, 'rootfs',
627        'install_operations', new_rootfs_part,
628        self.payload.manifest.new_rootfs_info, old_rootfs_part,
629        self.payload.manifest.old_rootfs_info)
630
631    # Apply update to kernel update.
632    self._ApplyToPartition(
633        self.payload.manifest.kernel_install_operations, 'kernel',
634        'kernel_install_operations', new_kernel_part,
635        self.payload.manifest.new_kernel_info, old_kernel_part,
636        self.payload.manifest.old_kernel_info)
637