components/openstack/swift/patches/CVE-2016-0737.patch
changeset 5445 9b7b887c6955
equal deleted inserted replaced
5444:9f72ce29e7f4 5445:9b7b887c6955
       
     1 This upstream patch addresses CVE-2016-0737 which is filed under
       
     2 Launchpad bug 1466549. The issue is addressed in Swift 2.3.1 as well as
       
     3 2.5.1 or later.
       
     4 
       
     5 From 036c2f348d24c01c7a4deba3e44889c45270b46d Mon Sep 17 00:00:00 2001
       
     6 From: Samuel Merritt <[email protected]>
       
     7 Date: Thu, 18 Jun 2015 12:58:03 -0700
       
     8 Subject: Get better at closing WSGI iterables.
       
     9 
       
    10 PEP 333 (WSGI) says: "If the iterable returned by the application has
       
    11 a close() method, the server or gateway must call that method upon
       
    12 completion of the current request[.]"
       
    13 
       
    14 There's a bunch of places where we weren't doing that; some of them
       
    15 matter more than others. Calling .close() can prevent a connection
       
    16 leak in some cases. In others, it just provides a certain pedantic
       
    17 smugness. Either way, we should do what WSGI requires.
       
    18 
       
    19 Noteworthy goofs include:
       
    20 
       
    21   * If a client is downloading a large object and disconnects halfway
       
    22     through, a proxy -> obj connection may be leaked. In this case,
       
    23     the WSGI iterable is a SegmentedIterable, which lacked a close()
       
    24     method. Thus, when the WSGI server noticed the client disconnect,
       
    25     it had no way of telling the SegmentedIterable about it, and so
       
    26     the underlying iterable for the segment's data didn't get
       
    27     closed.
       
    28 
       
    29     Here, it seems likely (though unproven) that the object server
       
    30     would time out and kill the connection, or that a
       
    31     ChunkWriteTimeout would fire down in the proxy server, so the
       
    32     leaked connection would eventually go away. However, a flurry of
       
    33     client disconnects could leave a big pile of useless connections.
       
    34 
       
    35   * If a conditional request receives a 304 or 412, the underlying
       
    36     app_iter is not closed. This mostly affects conditional requests
       
    37     for large objects.
       
    38 
       
    39 The leaked connections were noticed by this patch's co-author, who
       
    40 made the changes to SegmentedIterable. Those changes helped, but did
       
    41 not completely fix, the issue. The rest of the patch is an attempt to
       
    42 plug the rest of the holes.
       
    43 
       
    44 Co-Authored-By: Romain LE DISEZ <[email protected]>
       
    45 
       
    46 Closes-Bug: #1466549
       
    47 
       
    48 Change-Id: I168e147aae7c1728e7e3fdabb7fba6f2d747d937
       
    49 (cherry picked from commit 12d8a53fffea6e4bed8ba3d502ce625f5c6710b9
       
    50 with fixed import conflicts)
       
    51 ---
       
    52  swift/common/middleware/dlo.py          |  8 ++++++--
       
    53  swift/common/middleware/slo.py          | 10 ++++++----
       
    54  swift/common/request_helpers.py         | 35 ++++++++++++---------------------
       
    55  swift/common/swob.py                    |  9 ++++++++-
       
    56  swift/common/utils.py                   | 22 +++++++++++++++++++++
       
    57  swift/proxy/controllers/obj.py          |  4 ++--
       
    58  test/unit/common/middleware/helpers.py  | 32 +++++++++++++++++++++++++++++-
       
    59  test/unit/common/middleware/test_dlo.py | 10 ++++++++--
       
    60  test/unit/common/middleware/test_slo.py | 13 ++++++++----
       
    61  9 files changed, 105 insertions(+), 38 deletions(-)
       
    62 
       
    63 diff --git a/swift/common/middleware/dlo.py b/swift/common/middleware/dlo.py
       
    64 index d2761ac..9330ccb 100644
       
    65 --- a/swift/common/middleware/dlo.py
       
    66 +++ b/swift/common/middleware/dlo.py
       
    67 @@ -22,7 +22,8 @@ from swift.common.http import is_success
       
    68  from swift.common.swob import Request, Response, \
       
    69      HTTPRequestedRangeNotSatisfiable, HTTPBadRequest, HTTPConflict
       
    70  from swift.common.utils import get_logger, json, \
       
    71 -    RateLimitedIterator, read_conf_dir, quote
       
    72 +    RateLimitedIterator, read_conf_dir, quote, close_if_possible, \
       
    73 +    closing_if_possible
       
    74  from swift.common.request_helpers import SegmentedIterable
       
    75  from swift.common.wsgi import WSGIContext, make_subrequest
       
    76  from urllib import unquote
       
    77 @@ -48,7 +49,8 @@ class GetContext(WSGIContext):
       
    78          con_resp = con_req.get_response(self.dlo.app)
       
    79          if not is_success(con_resp.status_int):
       
    80              return con_resp, None
       
    81 -        return None, json.loads(''.join(con_resp.app_iter))
       
    82 +        with closing_if_possible(con_resp.app_iter):
       
    83 +            return None, json.loads(''.join(con_resp.app_iter))
       
    84  
       
    85      def _segment_listing_iterator(self, req, version, account, container,
       
    86                                    prefix, segments, first_byte=None,
       
    87 @@ -107,6 +109,7 @@ class GetContext(WSGIContext):
       
    88                  # we've already started sending the response body to the
       
    89                  # client, so all we can do is raise an exception to make the
       
    90                  # WSGI server close the connection early
       
    91 +                close_if_possible(error_response.app_iter)
       
    92                  raise ListingIterError(
       
    93                      "Got status %d listing container /%s/%s" %
       
    94                      (error_response.status_int, account, container))
       
    95 @@ -233,6 +236,7 @@ class GetContext(WSGIContext):
       
    96          # make sure this response is for a dynamic large object manifest
       
    97          for header, value in self._response_headers:
       
    98              if (header.lower() == 'x-object-manifest'):
       
    99 +                close_if_possible(resp_iter)
       
   100                  response = self.get_or_head_response(req, value)
       
   101                  return response(req.environ, start_response)
       
   102          else:
       
   103 diff --git a/swift/common/middleware/slo.py b/swift/common/middleware/slo.py
       
   104 index e8f1707..c7a97a6 100644
       
   105 --- a/swift/common/middleware/slo.py
       
   106 +++ b/swift/common/middleware/slo.py
       
   107 @@ -147,9 +147,9 @@ from swift.common.swob import Request, HTTPBadRequest, HTTPServerError, \
       
   108      Response
       
   109  from swift.common.utils import json, get_logger, config_true_value, \
       
   110      get_valid_utf8_str, override_bytes_from_content_type, split_path, \
       
   111 -    register_swift_info, RateLimitedIterator, quote
       
   112 -from swift.common.request_helpers import SegmentedIterable, \
       
   113 -    closing_if_possible, close_if_possible
       
   114 +    register_swift_info, RateLimitedIterator, quote, close_if_possible, \
       
   115 +    closing_if_possible
       
   116 +from swift.common.request_helpers import SegmentedIterable
       
   117  from swift.common.constraints import check_utf8, MAX_BUFFERED_SLO_SEGMENTS
       
   118  from swift.common.http import HTTP_NOT_FOUND, HTTP_UNAUTHORIZED, is_success
       
   119  from swift.common.wsgi import WSGIContext, make_subrequest
       
   120 @@ -227,6 +227,7 @@ class SloGetContext(WSGIContext):
       
   121          sub_resp = sub_req.get_response(self.slo.app)
       
   122  
       
   123          if not is_success(sub_resp.status_int):
       
   124 +            close_if_possible(sub_resp.app_iter)
       
   125              raise ListingIterError(
       
   126                  'ERROR: while fetching %s, GET of submanifest %s '
       
   127                  'failed with status %d' % (req.path, sub_req.path,
       
   128 @@ -400,7 +401,8 @@ class SloGetContext(WSGIContext):
       
   129          return response(req.environ, start_response)
       
   130  
       
   131      def get_or_head_response(self, req, resp_headers, resp_iter):
       
   132 -        resp_body = ''.join(resp_iter)
       
   133 +        with closing_if_possible(resp_iter):
       
   134 +            resp_body = ''.join(resp_iter)
       
   135          try:
       
   136              segments = json.loads(resp_body)
       
   137          except ValueError:
       
   138 diff --git a/swift/common/request_helpers.py b/swift/common/request_helpers.py
       
   139 index 14b9fd8..8aa8457 100644
       
   140 --- a/swift/common/request_helpers.py
       
   141 +++ b/swift/common/request_helpers.py
       
   142 @@ -23,7 +23,6 @@ from swob in here without creating circular imports.
       
   143  import hashlib
       
   144  import itertools
       
   145  import time
       
   146 -from contextlib import contextmanager
       
   147  from urllib import unquote
       
   148  from swift import gettext_ as _
       
   149  from swift.common.storage_policy import POLICIES
       
   150 @@ -32,7 +31,8 @@ from swift.common.exceptions import ListingIterError, SegmentError
       
   151  from swift.common.http import is_success
       
   152  from swift.common.swob import (HTTPBadRequest, HTTPNotAcceptable,
       
   153                                 HTTPServiceUnavailable)
       
   154 -from swift.common.utils import split_path, validate_device_partition
       
   155 +from swift.common.utils import split_path, validate_device_partition, \
       
   156 +    close_if_possible
       
   157  from swift.common.wsgi import make_subrequest
       
   158  
       
   159  
       
   160 @@ -249,26 +249,6 @@ def copy_header_subset(from_r, to_r, condition):
       
   161              to_r.headers[k] = v
       
   162  
       
   163  
       
   164 -def close_if_possible(maybe_closable):
       
   165 -    close_method = getattr(maybe_closable, 'close', None)
       
   166 -    if callable(close_method):
       
   167 -        return close_method()
       
   168 -
       
   169 -
       
   170 -@contextmanager
       
   171 -def closing_if_possible(maybe_closable):
       
   172 -    """
       
   173 -    Like contextlib.closing(), but doesn't crash if the object lacks a close()
       
   174 -    method.
       
   175 -
       
   176 -    PEP 333 (WSGI) says: "If the iterable returned by the application has a
       
   177 -    close() method, the server or gateway must call that method upon
       
   178 -    completion of the current request[.]" This function makes that easier.
       
   179 -    """
       
   180 -    yield maybe_closable
       
   181 -    close_if_possible(maybe_closable)
       
   182 -
       
   183 -
       
   184  class SegmentedIterable(object):
       
   185      """
       
   186      Iterable that returns the object contents for a large object.
       
   187 @@ -304,6 +284,7 @@ class SegmentedIterable(object):
       
   188          self.peeked_chunk = None
       
   189          self.app_iter = self._internal_iter()
       
   190          self.validated_first_segment = False
       
   191 +        self.current_resp = None
       
   192  
       
   193      def _internal_iter(self):
       
   194          start_time = time.time()
       
   195 @@ -360,6 +341,8 @@ class SegmentedIterable(object):
       
   196                           'r_size': seg_resp.content_length,
       
   197                           's_etag': seg_etag,
       
   198                           's_size': seg_size})
       
   199 +                else:
       
   200 +                    self.current_resp = seg_resp
       
   201  
       
   202                  seg_hash = hashlib.md5()
       
   203                  for chunk in seg_resp.app_iter:
       
   204 @@ -431,3 +414,11 @@ class SegmentedIterable(object):
       
   205              return itertools.chain([pc], self.app_iter)
       
   206          else:
       
   207              return self.app_iter
       
   208 +
       
   209 +    def close(self):
       
   210 +        """
       
   211 +        Called when the client disconnect. Ensure that the connection to the
       
   212 +        backend server is closed.
       
   213 +        """
       
   214 +        if self.current_resp:
       
   215 +            close_if_possible(self.current_resp.app_iter)
       
   216 diff --git a/swift/common/swob.py b/swift/common/swob.py
       
   217 index c2e3afb..a1bd9f6 100644
       
   218 --- a/swift/common/swob.py
       
   219 +++ b/swift/common/swob.py
       
   220 @@ -49,7 +49,8 @@ import random
       
   221  import functools
       
   222  import inspect
       
   223  
       
   224 -from swift.common.utils import reiterate, split_path, Timestamp, pairs
       
   225 +from swift.common.utils import reiterate, split_path, Timestamp, pairs, \
       
   226 +    close_if_possible
       
   227  from swift.common.exceptions import InvalidTimestamp
       
   228  
       
   229  
       
   230 @@ -1203,12 +1204,14 @@ class Response(object):
       
   231                      etag in self.request.if_none_match:
       
   232                  self.status = 304
       
   233                  self.content_length = 0
       
   234 +                close_if_possible(app_iter)
       
   235                  return ['']
       
   236  
       
   237              if etag and self.request.if_match and \
       
   238                 etag not in self.request.if_match:
       
   239                  self.status = 412
       
   240                  self.content_length = 0
       
   241 +                close_if_possible(app_iter)
       
   242                  return ['']
       
   243  
       
   244              if self.status_int == 404 and self.request.if_match \
       
   245 @@ -1219,18 +1222,21 @@ class Response(object):
       
   246                  # Failed) response. [RFC 2616 section 14.24]
       
   247                  self.status = 412
       
   248                  self.content_length = 0
       
   249 +                close_if_possible(app_iter)
       
   250                  return ['']
       
   251  
       
   252              if self.last_modified and self.request.if_modified_since \
       
   253                 and self.last_modified <= self.request.if_modified_since:
       
   254                  self.status = 304
       
   255                  self.content_length = 0
       
   256 +                close_if_possible(app_iter)
       
   257                  return ['']
       
   258  
       
   259              if self.last_modified and self.request.if_unmodified_since \
       
   260                 and self.last_modified > self.request.if_unmodified_since:
       
   261                  self.status = 412
       
   262                  self.content_length = 0
       
   263 +                close_if_possible(app_iter)
       
   264                  return ['']
       
   265  
       
   266          if self.request and self.request.method == 'HEAD':
       
   267 @@ -1244,6 +1250,7 @@ class Response(object):
       
   268              if ranges == []:
       
   269                  self.status = 416
       
   270                  self.content_length = 0
       
   271 +                close_if_possible(app_iter)
       
   272                  return ['']
       
   273              elif ranges:
       
   274                  range_size = len(ranges)
       
   275 diff --git a/swift/common/utils.py b/swift/common/utils.py
       
   276 index 19dcfd3..85c3824 100644
       
   277 --- a/swift/common/utils.py
       
   278 +++ b/swift/common/utils.py
       
   279 @@ -3143,6 +3143,28 @@ def ismount_raw(path):
       
   280      return False
       
   281  
       
   282  
       
   283 +def close_if_possible(maybe_closable):
       
   284 +    close_method = getattr(maybe_closable, 'close', None)
       
   285 +    if callable(close_method):
       
   286 +        return close_method()
       
   287 +
       
   288 +
       
   289 +@contextmanager
       
   290 +def closing_if_possible(maybe_closable):
       
   291 +    """
       
   292 +    Like contextlib.closing(), but doesn't crash if the object lacks a close()
       
   293 +    method.
       
   294 +
       
   295 +    PEP 333 (WSGI) says: "If the iterable returned by the application has a
       
   296 +    close() method, the server or gateway must call that method upon
       
   297 +    completion of the current request[.]" This function makes that easier.
       
   298 +    """
       
   299 +    try:
       
   300 +        yield maybe_closable
       
   301 +    finally:
       
   302 +        close_if_possible(maybe_closable)
       
   303 +
       
   304 +
       
   305  _rfc_token = r'[^()<>@,;:\"/\[\]?={}\x00-\x20\x7f]+'
       
   306  _rfc_extension_pattern = re.compile(
       
   307      r'(?:\s*;\s*(' + _rfc_token + r")\s*(?:=\s*(" + _rfc_token +
       
   308 diff --git a/swift/proxy/controllers/obj.py b/swift/proxy/controllers/obj.py
       
   309 index a83242b..784dfef 100644
       
   310 --- a/swift/proxy/controllers/obj.py
       
   311 +++ b/swift/proxy/controllers/obj.py
       
   312 @@ -43,7 +43,7 @@ from swift.common.utils import (
       
   313      clean_content_type, config_true_value, ContextPool, csv_append,
       
   314      GreenAsyncPile, GreenthreadSafeIterator, json, Timestamp,
       
   315      normalize_delete_at_timestamp, public, get_expirer_container,
       
   316 -    quorum_size)
       
   317 +    quorum_size, close_if_possible)
       
   318  from swift.common.bufferedhttp import http_connect
       
   319  from swift.common.constraints import check_metadata, check_object_creation, \
       
   320      check_copy_from_header, check_destination_header, \
       
   321 @@ -68,7 +68,7 @@ from swift.common.swob import HTTPAccepted, HTTPBadRequest, HTTPNotFound, \
       
   322      HTTPServerError, HTTPServiceUnavailable, Request, HeaderKeyDict, \
       
   323      HTTPClientDisconnect, HTTPUnprocessableEntity, Response, HTTPException
       
   324  from swift.common.request_helpers import is_sys_or_user_meta, is_sys_meta, \
       
   325 -    remove_items, copy_header_subset, close_if_possible
       
   326 +    remove_items, copy_header_subset
       
   327  
       
   328  
       
   329  def copy_headers_into(from_r, to_r):
       
   330 diff --git a/test/unit/common/middleware/helpers.py b/test/unit/common/middleware/helpers.py
       
   331 index 68a4bfe..7c1b455 100644
       
   332 --- a/test/unit/common/middleware/helpers.py
       
   333 +++ b/test/unit/common/middleware/helpers.py
       
   334 @@ -15,6 +15,7 @@
       
   335  
       
   336  # This stuff can't live in test/unit/__init__.py due to its swob dependency.
       
   337  
       
   338 +from collections import defaultdict
       
   339  from copy import deepcopy
       
   340  from hashlib import md5
       
   341  from swift.common import swob
       
   342 @@ -23,6 +24,20 @@ from swift.common.utils import split_path
       
   343  from test.unit import FakeLogger, FakeRing
       
   344  
       
   345  
       
   346 +class LeakTrackingIter(object):
       
   347 +    def __init__(self, inner_iter, fake_swift, path):
       
   348 +        self.inner_iter = inner_iter
       
   349 +        self.fake_swift = fake_swift
       
   350 +        self.path = path
       
   351 +
       
   352 +    def __iter__(self):
       
   353 +        for x in self.inner_iter:
       
   354 +            yield x
       
   355 +
       
   356 +    def close(self):
       
   357 +        self.fake_swift.mark_closed(self.path)
       
   358 +
       
   359 +
       
   360  class FakeSwift(object):
       
   361      """
       
   362      A good-enough fake Swift proxy server to use in testing middleware.
       
   363 @@ -30,6 +45,7 @@ class FakeSwift(object):
       
   364  
       
   365      def __init__(self):
       
   366          self._calls = []
       
   367 +        self._unclosed_req_paths = defaultdict(int)
       
   368          self.req_method_paths = []
       
   369          self.swift_sources = []
       
   370          self.uploaded = {}
       
   371 @@ -105,7 +121,21 @@ class FakeSwift(object):
       
   372          req = swob.Request(env)
       
   373          resp = resp_class(req=req, headers=headers, body=body,
       
   374                            conditional_response=True)
       
   375 -        return resp(env, start_response)
       
   376 +        wsgi_iter = resp(env, start_response)
       
   377 +        self.mark_opened(path)
       
   378 +        return LeakTrackingIter(wsgi_iter, self, path)
       
   379 +
       
   380 +    def mark_opened(self, path):
       
   381 +        self._unclosed_req_paths[path] += 1
       
   382 +
       
   383 +    def mark_closed(self, path):
       
   384 +        self._unclosed_req_paths[path] -= 1
       
   385 +
       
   386 +    @property
       
   387 +    def unclosed_requests(self):
       
   388 +        return {path: count
       
   389 +                for path, count in self._unclosed_req_paths.items()
       
   390 +                if count > 0}
       
   391  
       
   392      @property
       
   393      def calls(self):
       
   394 diff --git a/test/unit/common/middleware/test_dlo.py b/test/unit/common/middleware/test_dlo.py
       
   395 index 16237eb..119e4ab 100644
       
   396 --- a/test/unit/common/middleware/test_dlo.py
       
   397 +++ b/test/unit/common/middleware/test_dlo.py
       
   398 @@ -26,6 +26,7 @@ import unittest
       
   399  
       
   400  from swift.common import exceptions, swob
       
   401  from swift.common.middleware import dlo
       
   402 +from swift.common.utils import closing_if_possible
       
   403  from test.unit.common.middleware.helpers import FakeSwift
       
   404  
       
   405  
       
   406 @@ -54,8 +55,10 @@ class DloTestCase(unittest.TestCase):
       
   407          body = ''
       
   408          caught_exc = None
       
   409          try:
       
   410 -            for chunk in body_iter:
       
   411 -                body += chunk
       
   412 +            # appease the close-checker
       
   413 +            with closing_if_possible(body_iter):
       
   414 +                for chunk in body_iter:
       
   415 +                    body += chunk
       
   416          except Exception as exc:
       
   417              if expect_exception:
       
   418                  caught_exc = exc
       
   419 @@ -279,6 +282,9 @@ class TestDloHeadManifest(DloTestCase):
       
   420  
       
   421  
       
   422  class TestDloGetManifest(DloTestCase):
       
   423 +    def tearDown(self):
       
   424 +        self.assertEqual(self.app.unclosed_requests, {})
       
   425 +
       
   426      def test_get_manifest(self):
       
   427          expected_etag = '"%s"' % md5hex(
       
   428              md5hex("aaaaa") + md5hex("bbbbb") + md5hex("ccccc") +
       
   429 diff --git a/test/unit/common/middleware/test_slo.py b/test/unit/common/middleware/test_slo.py
       
   430 index 4160d91..4d483c8 100644
       
   431 --- a/test/unit/common/middleware/test_slo.py
       
   432 +++ b/test/unit/common/middleware/test_slo.py
       
   433 @@ -24,7 +24,7 @@ from swift.common import swob, utils
       
   434  from swift.common.exceptions import ListingIterError, SegmentError
       
   435  from swift.common.middleware import slo
       
   436  from swift.common.swob import Request, Response, HTTPException
       
   437 -from swift.common.utils import json
       
   438 +from swift.common.utils import json, closing_if_possible
       
   439  from test.unit.common.middleware.helpers import FakeSwift
       
   440  
       
   441  
       
   442 @@ -74,8 +74,10 @@ class SloTestCase(unittest.TestCase):
       
   443          body = ''
       
   444          caught_exc = None
       
   445          try:
       
   446 -            for chunk in body_iter:
       
   447 -                body += chunk
       
   448 +            # appease the close-checker
       
   449 +            with closing_if_possible(body_iter):
       
   450 +                for chunk in body_iter:
       
   451 +                    body += chunk
       
   452          except Exception as exc:
       
   453              if expect_exception:
       
   454                  caught_exc = exc
       
   455 @@ -222,7 +224,7 @@ class TestSloPutManifest(SloTestCase):
       
   456              '/?multipart-manifest=put',
       
   457              environ={'REQUEST_METHOD': 'PUT'}, body=test_json_data)
       
   458          self.assertEquals(
       
   459 -            self.slo.handle_multipart_put(req, fake_start_response),
       
   460 +            list(self.slo.handle_multipart_put(req, fake_start_response)),
       
   461              ['passed'])
       
   462  
       
   463      def test_handle_multipart_put_success(self):
       
   464 @@ -844,6 +846,9 @@ class TestSloGetManifest(SloTestCase):
       
   465                            'X-Object-Meta-Fish': 'Bass'},
       
   466              "[not {json (at ++++all")
       
   467  
       
   468 +    def tearDown(self):
       
   469 +        self.assertEqual(self.app.unclosed_requests, {})
       
   470 +
       
   471      def test_get_manifest_passthrough(self):
       
   472          req = Request.blank(
       
   473              '/v1/AUTH_test/gettest/manifest-bc?multipart-manifest=get',
       
   474 -- 
       
   475 cgit v0.11.2
       
   476