Moderate severityNVD Advisory· Published Jan 23, 2015· Updated May 6, 2026
CVE-2014-9623
CVE-2014-9623
Description
OpenStack Glance 2014.2.x through 2014.2.1, 2014.1.3, and earlier allows remote authenticated users to bypass the storage quota and cause a denial of service (disk consumption) by deleting an image in the saving state.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
glancePyPI | < 11.0.0a0 | 11.0.0a0 |
Affected products
1Patches
30dc8fbb3479aCleanup chunks for deleted image that was 'saving'
15 files changed · +101 −84
glance/api/authorization.py+2 −2 modified@@ -161,10 +161,10 @@ def add(self, image_member): raise exception.Forbidden(message % self.image.image_id) - def save(self, image_member): + def save(self, image_member, from_state=None): if (self.context.is_admin or self.context.owner == image_member.member_id): - self.member_repo.save(image_member) + self.member_repo.save(image_member, from_state=from_state) else: message = _("You cannot update image member %s") raise exception.Forbidden(message % image_member.member_id)
glance/api/policy.py+4 −4 modified@@ -118,9 +118,9 @@ def list(self, *args, **kwargs): self.policy.enforce(self.context, 'get_images', {}) return super(ImageRepoProxy, self).list(*args, **kwargs) - def save(self, image): + def save(self, image, from_state=None): self.policy.enforce(self.context, 'modify_image', {}) - return super(ImageRepoProxy, self).save(image) + return super(ImageRepoProxy, self).save(image, from_state=from_state) def add(self, image): self.policy.enforce(self.context, 'add_image', {}) @@ -221,9 +221,9 @@ def get(self, member_id): self.policy.enforce(self.context, 'get_member', {}) return self.member_repo.get(member_id) - def save(self, member): + def save(self, member, from_state=None): self.policy.enforce(self.context, 'modify_member', {}) - self.member_repo.save(member) + self.member_repo.save(member, from_state=from_state) def list(self, *args, **kwargs): self.policy.enforce(self.context, 'get_members', {})
glance/api/v1/upload_utils.py+15 −8 modified@@ -153,14 +153,21 @@ def _kill_mismatched(image_meta, attr, actual): update_data = {'checksum': checksum, 'size': size} try: - image_meta = registry.update_image_metadata(req.context, - image_id, - update_data, - from_state='saving') - - except exception.NotFound as e: - msg = _("Image %s could not be found after upload. The image may" - " have been deleted during the upload.") % image_id + try: + state = 'saving' + image_meta = registry.update_image_metadata(req.context, + image_id, + update_data, + from_state=state) + except exception.Duplicate: + image = registry.get_image_metadata(req.context, image_id) + if image['status'] == 'deleted': + raise exception.NotFound() + else: + raise + except exception.NotFound: + msg = _LI("Image %s could not be found after upload. The image may" + " have been deleted during the upload.") % image_id LOG.info(msg) # NOTE(jculp): we need to clean up the datastore if an image
glance/api/v2/image_data.py+9 −7 modified@@ -72,14 +72,12 @@ def upload(self, req, image_id, data, size): try: image_repo.save(image) image.set_data(data, size) - image_repo.save(image) - except exception.NotFound as e: - msg = (_("Image %(id)s could not be found after upload." + image_repo.save(image, from_state='saving') + except (exception.NotFound, exception.Conflict): + msg = (_("Image %s could not be found after upload. " "The image may have been deleted during the " - "upload: %(error)s Cleaning up the chunks " - "uploaded") % - {'id': image_id, - 'error': utils.exception_to_str(e)}) + "upload, cleaning up the chunks uploaded.") % + image_id) LOG.warn(msg) # NOTE(sridevi): Cleaning up the uploaded chunks. try: @@ -152,6 +150,10 @@ def upload(self, req, image_id, data, size): raise webob.exc.HTTPServiceUnavailable(explanation=msg, request=req) + except webob.exc.HTTPGone as e: + with excutils.save_and_reraise_exception(): + LOG.error(_LE("Failed to upload image data due to HTTP error")) + except webob.exc.HTTPError as e: with excutils.save_and_reraise_exception(): LOG.error(_LE("Failed to upload image data due to HTTP error"))
glance/db/__init__.py+4 −3 modified@@ -166,15 +166,16 @@ def add(self, image): image.created_at = new_values['created_at'] image.updated_at = new_values['updated_at'] - def save(self, image): + def save(self, image, from_state=None): image_values = self._format_image_to_db(image) if image_values['size'] > CONF.image_size_cap: raise exception.ImageSizeLimitExceeded try: new_values = self.db_api.image_update(self.context, image.image_id, image_values, - purge_props=True) + purge_props=True, + from_state=from_state) except (exception.NotFound, exception.Forbidden): msg = _("No image found with ID %s") % image.image_id raise exception.NotFound(msg) @@ -267,7 +268,7 @@ def remove(self, image_member): msg = _("The specified member %s could not be found") raise exception.NotFound(msg % image_member.id) - def save(self, image_member): + def save(self, image_member, from_state=None): image_member_values = self._format_image_member_to_db(image_member) try: new_values = self.db_api.image_member_update(self.context,
glance/domain/proxy.py+2 −2 modified@@ -94,9 +94,9 @@ def add(self, item): result = self.base.add(base_item) return self.helper.proxy(result) - def save(self, item): + def save(self, item, from_state=None): base_item = self.helper.unproxy(item) - result = self.base.save(base_item) + result = self.base.save(base_item, from_state=from_state) return self.helper.proxy(result) def remove(self, item):
glance/location.py+2 −2 modified@@ -61,8 +61,8 @@ def add(self, image): self._set_acls(image) return result - def save(self, image): - result = super(ImageRepoProxy, self).save(image) + def save(self, image, from_state=None): + result = super(ImageRepoProxy, self).save(image, from_state=from_state) self._set_acls(image) return result
glance/notifier.py+2 −2 modified@@ -125,8 +125,8 @@ def __init__(self, image_repo, context, notifier): item_proxy_class=ImageProxy, item_proxy_kwargs=proxy_kwargs) - def save(self, image): - super(ImageRepoProxy, self).save(image) + def save(self, image, from_state=None): + super(ImageRepoProxy, self).save(image, from_state=from_state) self.notifier.info('image.update', format_image_notification(image))
glance/quota/__init__.py+2 −2 modified@@ -104,10 +104,10 @@ def _enforce_image_property_quota(self, attempted): LOG.debug(six.text_type(exc)) raise exc - def save(self, image): + def save(self, image, from_state=None): if image.added_new_properties(): self._enforce_image_property_quota(len(image.extra_properties)) - return super(ImageRepoProxy, self).save(image) + return super(ImageRepoProxy, self).save(image, from_state=from_state) def add(self, image): self._enforce_image_property_quota(len(image.extra_properties))
glance/tests/unit/test_domain_proxy.py+7 −5 modified@@ -74,7 +74,7 @@ def test_add(self): self._test_method('add', 'snuff', 'enough') def test_save(self): - self._test_method('save', 'snuff', 'enough') + self._test_method('save', 'snuff', 'enough', from_state=None) def test_remove(self): self._test_method('add', None, 'flying') @@ -121,14 +121,14 @@ def test_list(self): self.assertEqual(tuple(), results[i].args) self.assertEqual({'a': 1}, results[i].kwargs) - def _test_method_with_proxied_argument(self, name, result): + def _test_method_with_proxied_argument(self, name, result, **kwargs): self.fake_repo.result = result item = FakeProxy('snoop') method = getattr(self.proxy_repo, name) proxy_result = method(item) self.assertEqual(('snoop',), self.fake_repo.args) - self.assertEqual({}, self.fake_repo.kwargs) + self.assertEqual(kwargs, self.fake_repo.kwargs) if result is None: self.assertIsNone(proxy_result) @@ -145,10 +145,12 @@ def test_add_with_no_result(self): self._test_method_with_proxied_argument('add', None) def test_save(self): - self._test_method_with_proxied_argument('save', 'dog') + self._test_method_with_proxied_argument('save', 'dog', + from_state=None) def test_save_with_no_result(self): - self._test_method_with_proxied_argument('save', None) + self._test_method_with_proxied_argument('save', None, + from_state=None) def test_remove(self): self._test_method_with_proxied_argument('remove', 'dog')
glance/tests/unit/test_policy.py+1 −1 modified@@ -78,7 +78,7 @@ def add(self, image_member): def get(self, *args, **kwargs): return 'member_repo_get' - def save(self, image_member): + def save(self, image_member, from_state=None): image_member.output = 'member_repo_save' def list(self, *args, **kwargs):
glance/tests/unit/test_quota.py+11 −6 modified@@ -366,7 +366,8 @@ def test_save_image_with_image_property(self): self.image.extra_properties = {'foo': 'bar'} self.image_repo_proxy.save(self.image) - self.image_repo_mock.save.assert_called_once_with(self.base_image) + self.image_repo_mock.save.assert_called_once_with(self.base_image, + from_state=None) def test_save_image_too_many_image_properties(self): self.config(image_property_quota=1) @@ -382,7 +383,8 @@ def test_save_image_unlimited_image_properties(self): self.image.extra_properties = {'foo': 'bar'} self.image_repo_proxy.save(self.image) - self.image_repo_mock.save.assert_called_once_with(self.base_image) + self.image_repo_mock.save.assert_called_once_with(self.base_image, + from_state=None) def test_add_image_with_image_property(self): self.config(image_property_quota=1) @@ -421,7 +423,8 @@ def test_modify_image_properties_when_quota_exceeded(self): self.config(image_property_quota=1) self.image.extra_properties = {'foo': 'frob', 'spam': 'eggs'} self.image_repo_proxy.save(self.image) - self.image_repo_mock.save.assert_called_once_with(self.base_image) + self.image_repo_mock.save.assert_called_once_with(self.base_image, + from_state=None) self.assertEqual('frob', self.base_image.extra_properties['foo']) self.assertEqual('eggs', self.base_image.extra_properties['spam']) @@ -430,7 +433,8 @@ def test_delete_image_properties_when_quota_exceeded(self): self.config(image_property_quota=1) del self.image.extra_properties['foo'] self.image_repo_proxy.save(self.image) - self.image_repo_mock.save.assert_called_once_with(self.base_image) + self.image_repo_mock.save.assert_called_once_with(self.base_image, + from_state=None) self.assertNotIn('foo', self.base_image.extra_properties) self.assertEqual('ham', self.base_image.extra_properties['spam']) @@ -452,7 +456,7 @@ def test_exceed_quota_during_patch_operation(self): del self.image.extra_properties['frob'] del self.image.extra_properties['lorem'] self.image_repo_proxy.save(self.image) - call_args = mock.call(self.base_image) + call_args = mock.call(self.base_image, from_state=None) self.assertEqual(call_args, self.image_repo_mock.save.call_args) self.assertEqual('bar', self.base_image.extra_properties['foo']) self.assertEqual('ham', self.base_image.extra_properties['spam']) @@ -471,7 +475,8 @@ def test_quota_exceeded_after_delete_image_properties(self): self.config(image_property_quota=1) del self.image.extra_properties['foo'] self.image_repo_proxy.save(self.image) - self.image_repo_mock.save.assert_called_once_with(self.base_image) + self.image_repo_mock.save.assert_called_once_with(self.base_image, + from_state=None) self.assertNotIn('foo', self.base_image.extra_properties) self.assertEqual('ham', self.base_image.extra_properties['spam']) self.assertEqual('baz', self.base_image.extra_properties['frob'])
glance/tests/unit/test_store_image.py+1 −1 modified@@ -35,7 +35,7 @@ class ImageRepoStub(object): def add(self, image): return image - def save(self, image): + def save(self, image, from_state=None): return image
glance/tests/unit/v1/test_api.py+25 −29 modified@@ -1741,17 +1741,16 @@ def mock_store_add_to_backend_w_exception(*args, **kwargs): self.assertEqual(1, mock_store_add_to_backend.call_count) - def test_delete_during_image_upload(self): - req = unit_test_utils.get_fake_request() + def _check_delete_during_image_upload(self, is_admin=False): fixture_headers = {'x-image-meta-store': 'file', 'x-image-meta-disk-format': 'vhd', 'x-image-meta-container-format': 'ovf', 'x-image-meta-name': 'fake image #3', 'x-image-meta-property-key1': 'value1'} - req = webob.Request.blank("/images") - req.method = 'POST' + req = unit_test_utils.get_fake_request(path="/images", + is_admin=is_admin) for k, v in six.iteritems(fixture_headers): req.headers[k] = v @@ -1776,31 +1775,18 @@ def mock_initiate_deletion(*args, **kwargs): mock_initiate_deletion) orig_update_image_metadata = registry.update_image_metadata - ctlr = glance.api.v1.controller.BaseController - orig_get_image_meta_or_404 = ctlr.get_image_meta_or_404 - - def mock_update_image_metadata(*args, **kwargs): - if args[2].get('status', None) == 'deleted': + data = "somedata" - # One shot. - def mock_get_image_meta_or_404(*args, **kwargs): - ret = orig_get_image_meta_or_404(*args, **kwargs) - ret['status'] = 'queued' - self.stubs.Set(ctlr, 'get_image_meta_or_404', - orig_get_image_meta_or_404) - return ret - - self.stubs.Set(ctlr, 'get_image_meta_or_404', - mock_get_image_meta_or_404) + def mock_update_image_metadata(*args, **kwargs): - req = webob.Request.blank("/images/%s" % image_id) - req.method = 'PUT' - req.headers['Content-Type'] = 'application/octet-stream' - req.body = "somedata" + if args[2].get('size', None) == len(data): + path = "/images/%s" % image_id + req = unit_test_utils.get_fake_request(path=path, + method='DELETE', + is_admin=is_admin) res = req.get_response(self.api) self.assertEqual(200, res.status_int) - self.assertFalse(res.location) self.stubs.Set(registry, 'update_image_metadata', orig_update_image_metadata) @@ -1810,20 +1796,30 @@ def mock_get_image_meta_or_404(*args, **kwargs): self.stubs.Set(registry, 'update_image_metadata', mock_update_image_metadata) - req = webob.Request.blank("/images/%s" % image_id) - req.method = 'DELETE' + req = unit_test_utils.get_fake_request(path="/images/%s" % image_id, + method='PUT') + req.headers['Content-Type'] = 'application/octet-stream' + req.body = data res = req.get_response(self.api) - self.assertEqual(200, res.status_int) + self.assertEqual(412, res.status_int) + self.assertFalse(res.location) self.assertTrue(called['initiate_deletion']) - req = webob.Request.blank("/images/%s" % image_id) - req.method = 'HEAD' + req = unit_test_utils.get_fake_request(path="/images/%s" % image_id, + method='HEAD', + is_admin=True) res = req.get_response(self.api) self.assertEqual(200, res.status_int) self.assertEqual('True', res.headers['x-image-meta-deleted']) self.assertEqual('deleted', res.headers['x-image-meta-status']) + def test_delete_during_image_upload_by_normal_user(self): + self._check_delete_during_image_upload(is_admin=False) + + def test_delete_during_image_upload_by_admin(self): + self._check_delete_during_image_upload(is_admin=True) + def test_disable_purge_props(self): """ Test the special x-glance-registry-purge-props header controls
glance/tests/unit/v2/test_image_data_resource.py+14 −10 modified@@ -79,7 +79,7 @@ def get(self, image_id): else: return self.result - def save(self, image): + def save(self, image, from_state=None): self.saved_image = image @@ -182,17 +182,21 @@ def test_upload_invalid(self): request, unit_test_utils.UUID1, 'YYYY', 4) def test_upload_non_existent_image_during_save_initiates_deletion(self): - def fake_save(self): + def fake_save_not_found(self): raise exception.NotFound() - request = unit_test_utils.get_fake_request() - image = FakeImage('abcd', locations=['http://example.com/image']) - self.image_repo.result = image - self.image_repo.save = fake_save - image.delete = mock.Mock() - self.assertRaises(webob.exc.HTTPGone, self.controller.upload, - request, str(uuid.uuid4()), 'ABC', 3) - self.assertTrue(image.delete.called) + def fake_save_conflict(self): + raise exception.Conflict() + + for fun in [fake_save_not_found, fake_save_conflict]: + request = unit_test_utils.get_fake_request() + image = FakeImage('abcd', locations=['http://example.com/image']) + self.image_repo.result = image + self.image_repo.save = fun + image.delete = mock.Mock() + self.assertRaises(webob.exc.HTTPGone, self.controller.upload, + request, str(uuid.uuid4()), 'ABC', 3) + self.assertTrue(image.delete.called) def test_upload_non_existent_image_raises_not_found_exception(self): def fake_save(self):
7d5d8657fd70Cleanup chunks for deleted image that was 'saving'
15 files changed · +98 −80
glance/api/authorization.py+2 −2 modified@@ -158,10 +158,10 @@ def add(self, image_member): raise exception.Forbidden(message % self.image.image_id) - def save(self, image_member): + def save(self, image_member, from_state=None): if (self.context.is_admin or self.context.owner == image_member.member_id): - self.member_repo.save(image_member) + self.member_repo.save(image_member, from_state=from_state) else: message = _("You cannot update image member %s") raise exception.Forbidden(message % image_member.member_id)
glance/api/policy.py+4 −4 modified@@ -182,9 +182,9 @@ def list(self, *args, **kwargs): self.policy.enforce(self.context, 'get_images', {}) return super(ImageRepoProxy, self).list(*args, **kwargs) - def save(self, image): + def save(self, image, from_state=None): self.policy.enforce(self.context, 'modify_image', {}) - return super(ImageRepoProxy, self).save(image) + return super(ImageRepoProxy, self).save(image, from_state=from_state) def add(self, image): self.policy.enforce(self.context, 'add_image', {}) @@ -285,9 +285,9 @@ def get(self, member_id): self.policy.enforce(self.context, 'get_member', {}) return self.member_repo.get(member_id) - def save(self, member): + def save(self, member, from_state=None): self.policy.enforce(self.context, 'modify_member', {}) - self.member_repo.save(member) + self.member_repo.save(member, from_state=from_state) def list(self, *args, **kwargs): self.policy.enforce(self.context, 'get_members', {})
glance/api/v1/upload_utils.py+13 −6 modified@@ -153,12 +153,19 @@ def _kill_mismatched(image_meta, attr, actual): update_data = {'checksum': checksum, 'size': size} try: - image_meta = registry.update_image_metadata(req.context, - image_id, - update_data, - from_state='saving') - - except exception.NotFound as e: + try: + state = 'saving' + image_meta = registry.update_image_metadata(req.context, + image_id, + update_data, + from_state=state) + except exception.Duplicate: + image = registry.get_image_metadata(req.context, image_id) + if image['status'] == 'deleted': + raise exception.NotFound() + else: + raise + except exception.NotFound: msg = _LI("Image %s could not be found after upload. The image may" " have been deleted during the upload.") % image_id LOG.info(msg)
glance/api/v2/image_data.py+6 −2 modified@@ -73,8 +73,8 @@ def upload(self, req, image_id, data, size): try: image_repo.save(image) image.set_data(data, size) - image_repo.save(image) - except exception.NotFound as e: + image_repo.save(image, from_state='saving') + except (exception.NotFound, exception.Conflict) as e: msg = (_("Image %(id)s could not be found after upload." "The image may have been deleted during the upload: " "%(error)s Cleaning up the chunks uploaded") % @@ -152,6 +152,10 @@ def upload(self, req, image_id, data, size): raise webob.exc.HTTPServiceUnavailable(explanation=msg, request=req) + except webob.exc.HTTPGone as e: + with excutils.save_and_reraise_exception(): + LOG.error(_LE("Failed to upload image data due to HTTP error")) + except webob.exc.HTTPError as e: with excutils.save_and_reraise_exception(): LOG.error(_LE("Failed to upload image data due to HTTP error"))
glance/db/__init__.py+4 −3 modified@@ -164,15 +164,16 @@ def add(self, image): image.created_at = new_values['created_at'] image.updated_at = new_values['updated_at'] - def save(self, image): + def save(self, image, from_state=None): image_values = self._format_image_to_db(image) if image_values['size'] > CONF.image_size_cap: raise exception.ImageSizeLimitExceeded try: new_values = self.db_api.image_update(self.context, image.image_id, image_values, - purge_props=True) + purge_props=True, + from_state=from_state) except (exception.NotFound, exception.Forbidden): msg = _("No image found with ID %s") % image.image_id raise exception.NotFound(msg) @@ -265,7 +266,7 @@ def remove(self, image_member): msg = _("The specified member %s could not be found") raise exception.NotFound(msg % image_member.id) - def save(self, image_member): + def save(self, image_member, from_state=None): image_member_values = self._format_image_member_to_db(image_member) try: new_values = self.db_api.image_member_update(self.context,
glance/domain/proxy.py+2 −2 modified@@ -94,9 +94,9 @@ def add(self, item): result = self.base.add(base_item) return self.helper.proxy(result) - def save(self, item): + def save(self, item, from_state=None): base_item = self.helper.unproxy(item) - result = self.base.save(base_item) + result = self.base.save(base_item, from_state=from_state) return self.helper.proxy(result) def remove(self, item):
glance/location.py+2 −2 modified@@ -60,8 +60,8 @@ def add(self, image): self._set_acls(image) return result - def save(self, image): - result = super(ImageRepoProxy, self).save(image) + def save(self, image, from_state=None): + result = super(ImageRepoProxy, self).save(image, from_state=from_state) self._set_acls(image) return result
glance/notifier.py+2 −2 modified@@ -122,8 +122,8 @@ def __init__(self, image_repo, context, notifier): item_proxy_class=ImageProxy, item_proxy_kwargs=proxy_kwargs) - def save(self, image): - super(ImageRepoProxy, self).save(image) + def save(self, image, from_state=None): + super(ImageRepoProxy, self).save(image, from_state=from_state) self.notifier.info('image.update', format_image_notification(image))
glance/quota/__init__.py+2 −2 modified@@ -104,10 +104,10 @@ def _enforce_image_property_quota(self, attempted): LOG.debug(six.text_type(exc)) raise exc - def save(self, image): + def save(self, image, from_state=None): if image.added_new_properties(): self._enforce_image_property_quota(len(image.extra_properties)) - return super(ImageRepoProxy, self).save(image) + return super(ImageRepoProxy, self).save(image, from_state=from_state) def add(self, image): self._enforce_image_property_quota(len(image.extra_properties))
glance/tests/unit/test_domain_proxy.py+8 −6 modified@@ -74,7 +74,7 @@ def test_add(self): self._test_method('add', 'snuff', 'enough') def test_save(self): - self._test_method('save', 'snuff', 'enough') + self._test_method('save', 'snuff', 'enough', from_state=None) def test_remove(self): self._test_method('add', None, 'flying') @@ -121,14 +121,14 @@ def test_list(self): self.assertEqual(results[i].args, tuple()) self.assertEqual(results[i].kwargs, {'a': 1}) - def _test_method_with_proxied_argument(self, name, result): + def _test_method_with_proxied_argument(self, name, result, **kwargs): self.fake_repo.result = result item = FakeProxy('snoop') method = getattr(self.proxy_repo, name) proxy_result = method(item) - self.assertEqual(self.fake_repo.args, ('snoop',)) - self.assertEqual(self.fake_repo.kwargs, {}) + self.assertEqual(('snoop',), self.fake_repo.args) + self.assertEqual(kwargs, self.fake_repo.kwargs) if result is None: self.assertIsNone(proxy_result) @@ -145,10 +145,12 @@ def test_add_with_no_result(self): self._test_method_with_proxied_argument('add', None) def test_save(self): - self._test_method_with_proxied_argument('save', 'dog') + self._test_method_with_proxied_argument('save', 'dog', + from_state=None) def test_save_with_no_result(self): - self._test_method_with_proxied_argument('save', None) + self._test_method_with_proxied_argument('save', None, + from_state=None) def test_remove(self): self._test_method_with_proxied_argument('remove', 'dog')
glance/tests/unit/test_policy.py+1 −1 modified@@ -78,7 +78,7 @@ def add(self, image_member): def get(self, *args, **kwargs): return 'member_repo_get' - def save(self, image_member): + def save(self, image_member, from_state=None): image_member.output = 'member_repo_save' def list(self, *args, **kwargs):
glance/tests/unit/test_quota.py+11 −6 modified@@ -367,7 +367,8 @@ def test_save_image_with_image_property(self): self.image.extra_properties = {'foo': 'bar'} self.image_repo_proxy.save(self.image) - self.image_repo_mock.save.assert_called_once_with(self.base_image) + self.image_repo_mock.save.assert_called_once_with(self.base_image, + from_state=None) def test_save_image_too_many_image_properties(self): self.config(image_property_quota=1) @@ -383,7 +384,8 @@ def test_save_image_unlimited_image_properties(self): self.image.extra_properties = {'foo': 'bar'} self.image_repo_proxy.save(self.image) - self.image_repo_mock.save.assert_called_once_with(self.base_image) + self.image_repo_mock.save.assert_called_once_with(self.base_image, + from_state=None) def test_add_image_with_image_property(self): self.config(image_property_quota=1) @@ -422,7 +424,8 @@ def test_modify_image_properties_when_quota_exceeded(self): self.config(image_property_quota=1) self.image.extra_properties = {'foo': 'frob', 'spam': 'eggs'} self.image_repo_proxy.save(self.image) - self.image_repo_mock.save.assert_called_once_with(self.base_image) + self.image_repo_mock.save.assert_called_once_with(self.base_image, + from_state=None) self.assertEqual('frob', self.base_image.extra_properties['foo']) self.assertEqual('eggs', self.base_image.extra_properties['spam']) @@ -431,7 +434,8 @@ def test_delete_image_properties_when_quota_exceeded(self): self.config(image_property_quota=1) del self.image.extra_properties['foo'] self.image_repo_proxy.save(self.image) - self.image_repo_mock.save.assert_called_once_with(self.base_image) + self.image_repo_mock.save.assert_called_once_with(self.base_image, + from_state=None) self.assertNotIn('foo', self.base_image.extra_properties) self.assertEqual('ham', self.base_image.extra_properties['spam']) @@ -447,7 +451,7 @@ def test_exceed_quota_during_patch_operation(self): del self.image.extra_properties['frob'] del self.image.extra_properties['lorem'] self.image_repo_proxy.save(self.image) - call_args = mock.call(self.base_image) + call_args = mock.call(self.base_image, from_state=None) self.assertEqual(call_args, self.image_repo_mock.save.call_args) self.assertEqual('bar', self.base_image.extra_properties['foo']) self.assertEqual('ham', self.base_image.extra_properties['spam']) @@ -466,7 +470,8 @@ def test_quota_exceeded_after_delete_image_properties(self): self.config(image_property_quota=1) del self.image.extra_properties['foo'] self.image_repo_proxy.save(self.image) - self.image_repo_mock.save.assert_called_once_with(self.base_image) + self.image_repo_mock.save.assert_called_once_with(self.base_image, + from_state=None) self.assertNotIn('foo', self.base_image.extra_properties) self.assertEqual('ham', self.base_image.extra_properties['spam']) self.assertEqual('baz', self.base_image.extra_properties['frob'])
glance/tests/unit/test_store_image.py+1 −1 modified@@ -36,7 +36,7 @@ class ImageRepoStub(object): def add(self, image): return image - def save(self, image): + def save(self, image, from_state=None): return image
glance/tests/unit/v1/test_api.py+26 −31 modified@@ -39,7 +39,6 @@ from glance.db.sqlalchemy import models as db_models from glance.openstack.common import jsonutils from glance.openstack.common import timeutils - import glance.registry.client.v1.api as registry from glance.tests.unit import base import glance.tests.unit.utils as unit_test_utils @@ -1735,17 +1734,16 @@ def mock_store_add_to_backend_w_exception(*args, **kwargs): self.assertEqual(1, mock_store_add_to_backend.call_count) - def test_delete_during_image_upload(self): - req = unit_test_utils.get_fake_request() + def _check_delete_during_image_upload(self, is_admin=False): fixture_headers = {'x-image-meta-store': 'file', 'x-image-meta-disk-format': 'vhd', 'x-image-meta-container-format': 'ovf', 'x-image-meta-name': 'fake image #3', 'x-image-meta-property-key1': 'value1'} - req = webob.Request.blank("/images") - req.method = 'POST' + req = unit_test_utils.get_fake_request(path="/images", + is_admin=is_admin) for k, v in six.iteritems(fixture_headers): req.headers[k] = v @@ -1770,31 +1768,18 @@ def mock_initiate_deletion(*args, **kwargs): mock_initiate_deletion) orig_update_image_metadata = registry.update_image_metadata - ctlr = glance.api.v1.controller.BaseController - orig_get_image_meta_or_404 = ctlr.get_image_meta_or_404 - - def mock_update_image_metadata(*args, **kwargs): - if args[2].get('status', None) == 'deleted': + data = "somedata" - # One shot. - def mock_get_image_meta_or_404(*args, **kwargs): - ret = orig_get_image_meta_or_404(*args, **kwargs) - ret['status'] = 'queued' - self.stubs.Set(ctlr, 'get_image_meta_or_404', - orig_get_image_meta_or_404) - return ret - - self.stubs.Set(ctlr, 'get_image_meta_or_404', - mock_get_image_meta_or_404) + def mock_update_image_metadata(*args, **kwargs): - req = webob.Request.blank("/images/%s" % image_id) - req.method = 'PUT' - req.headers['Content-Type'] = 'application/octet-stream' - req.body = "somedata" + if args[2].get('size', None) == len(data): + path = "/images/%s" % image_id + req = unit_test_utils.get_fake_request(path=path, + method='DELETE', + is_admin=is_admin) res = req.get_response(self.api) - self.assertEqual(res.status_int, 200) - self.assertFalse(res.location) + self.assertEqual(200, res.status_int) self.stubs.Set(registry, 'update_image_metadata', orig_update_image_metadata) @@ -1804,20 +1789,30 @@ def mock_get_image_meta_or_404(*args, **kwargs): self.stubs.Set(registry, 'update_image_metadata', mock_update_image_metadata) - req = webob.Request.blank("/images/%s" % image_id) - req.method = 'DELETE' + req = unit_test_utils.get_fake_request(path="/images/%s" % image_id, + method='PUT') + req.headers['Content-Type'] = 'application/octet-stream' + req.body = data res = req.get_response(self.api) - self.assertEqual(res.status_int, 200) + self.assertEqual(412, res.status_int) + self.assertFalse(res.location) self.assertTrue(called['initiate_deletion']) - req = webob.Request.blank("/images/%s" % image_id) - req.method = 'HEAD' + req = unit_test_utils.get_fake_request(path="/images/%s" % image_id, + method='HEAD', + is_admin=True) res = req.get_response(self.api) self.assertEqual(res.status_int, 200) self.assertEqual(res.headers['x-image-meta-deleted'], 'True') self.assertEqual(res.headers['x-image-meta-status'], 'deleted') + def test_delete_during_image_upload_by_normal_user(self): + self._check_delete_during_image_upload(is_admin=False) + + def test_delete_during_image_upload_by_admin(self): + self._check_delete_during_image_upload(is_admin=True) + def test_disable_purge_props(self): """ Test the special x-glance-registry-purge-props header controls
glance/tests/unit/v2/test_image_data_resource.py+14 −10 modified@@ -81,7 +81,7 @@ def get(self, image_id): else: return self.result - def save(self, image): + def save(self, image, from_state=None): self.saved_image = image @@ -184,17 +184,21 @@ def test_upload_invalid(self): request, unit_test_utils.UUID1, 'YYYY', 4) def test_upload_non_existent_image_during_save_initiates_deletion(self): - def fake_save(self): + def fake_save_not_found(self): raise exception.NotFound() - request = unit_test_utils.get_fake_request() - image = FakeImage('abcd', locations=['http://example.com/image']) - self.image_repo.result = image - self.image_repo.save = fake_save - image.delete = mock.Mock() - self.assertRaises(webob.exc.HTTPGone, self.controller.upload, - request, str(uuid.uuid4()), 'ABC', 3) - self.assertTrue(image.delete.called) + def fake_save_conflict(self): + raise exception.Conflict() + + for fun in [fake_save_not_found, fake_save_conflict]: + request = unit_test_utils.get_fake_request() + image = FakeImage('abcd', locations=['http://example.com/image']) + self.image_repo.result = image + self.image_repo.save = fun + image.delete = mock.Mock() + self.assertRaises(webob.exc.HTTPGone, self.controller.upload, + request, str(uuid.uuid4()), 'ABC', 3) + self.assertTrue(image.delete.called) def test_upload_non_existent_image_raises_not_found_exception(self): def fake_save(self):
f1260cc771eeCleanup chunks for deleted image that was 'saving'
15 files changed · +95 −78
glance/api/authorization.py+2 −2 modified@@ -147,10 +147,10 @@ def add(self, image_member): raise exception.Forbidden(message % self.image.image_id) - def save(self, image_member): + def save(self, image_member, from_state=None): if (self.context.is_admin or self.context.owner == image_member.member_id): - self.member_repo.save(image_member) + self.member_repo.save(image_member, from_state=from_state) else: message = _("You cannot update image member %s") raise exception.Forbidden(message % image_member.member_id)
glance/api/policy.py+4 −4 modified@@ -182,9 +182,9 @@ def list(self, *args, **kwargs): self.policy.enforce(self.context, 'get_images', {}) return super(ImageRepoProxy, self).list(*args, **kwargs) - def save(self, image): + def save(self, image, from_state=None): self.policy.enforce(self.context, 'modify_image', {}) - return super(ImageRepoProxy, self).save(image) + return super(ImageRepoProxy, self).save(image, from_state=from_state) def add(self, image): self.policy.enforce(self.context, 'add_image', {}) @@ -283,9 +283,9 @@ def get(self, member_id): self.policy.enforce(self.context, 'get_member', {}) return self.member_repo.get(member_id) - def save(self, member): + def save(self, member, from_state=None): self.policy.enforce(self.context, 'modify_member', {}) - self.member_repo.save(member) + self.member_repo.save(member, from_state=from_state) def list(self, *args, **kwargs): self.policy.enforce(self.context, 'get_members', {})
glance/api/v1/upload_utils.py+15 −8 modified@@ -146,14 +146,21 @@ def _kill_mismatched(image_meta, attr, actual): update_data = {'checksum': checksum, 'size': size} try: - image_meta = registry.update_image_metadata(req.context, - image_id, - update_data, - from_state='saving') - - except exception.NotFound as e: - msg = _("Image %s could not be found after upload. The image may " - "have been deleted during the upload.") % image_id + try: + state = 'saving' + image_meta = registry.update_image_metadata(req.context, + image_id, + update_data, + from_state=state) + except exception.Duplicate: + image = registry.get_image_metadata(req.context, image_id) + if image['status'] == 'deleted': + raise exception.NotFound() + else: + raise + except exception.NotFound: + msg = _("Image %s could not be found after upload. The image may" + " have been deleted during the upload.") % image_id LOG.info(msg) # NOTE(jculp): we need to clean up the datastore if an image
glance/api/v2/image_data.py+11 −7 modified@@ -22,6 +22,7 @@ import glance.db import glance.gateway import glance.notifier +from glance.openstack.common import excutils import glance.openstack.common.log as logging import glance.store @@ -66,13 +67,12 @@ def upload(self, req, image_id, data, size): try: image_repo.save(image) image.set_data(data, size) - image_repo.save(image) - except exception.NotFound as e: - msg = (_("Image %(id)s could not be found after upload." - "The image may have been deleted during the upload: " - "%(error)s Cleaning up the chunks uploaded") % - {'id': image_id, - 'error': e}) + image_repo.save(image, from_state='saving') + except (exception.NotFound, exception.Conflict): + msg = (_("Image %s could not be found after upload. " + "The image may have been deleted during the " + "upload, cleaning up the chunks uploaded.") % + image_id) LOG.warn(msg) # NOTE(sridevi): Cleaning up the uploaded chunks. try: @@ -131,6 +131,10 @@ def upload(self, req, image_id, data, size): raise webob.exc.HTTPServiceUnavailable(explanation=msg, request=req) + except webob.exc.HTTPGone as e: + with excutils.save_and_reraise_exception(): + LOG.error(_("Failed to upload image data due to HTTP error")) + except webob.exc.HTTPError as e: LOG.error(_("Failed to upload image data due to HTTP error")) self._restore(image_repo, image)
glance/db/__init__.py+4 −3 modified@@ -162,15 +162,16 @@ def add(self, image): image.created_at = new_values['created_at'] image.updated_at = new_values['updated_at'] - def save(self, image): + def save(self, image, from_state=None): image_values = self._format_image_to_db(image) if image_values['size'] > CONF.image_size_cap: raise exception.ImageSizeLimitExceeded try: new_values = self.db_api.image_update(self.context, image.image_id, image_values, - purge_props=True) + purge_props=True, + from_state=from_state) except (exception.NotFound, exception.Forbidden): msg = _("No image found with ID %s") % image.image_id raise exception.NotFound(msg) @@ -263,7 +264,7 @@ def remove(self, image_member): msg = _("The specified member %s could not be found") raise exception.NotFound(msg % image_member.id) - def save(self, image_member): + def save(self, image_member, from_state=None): image_member_values = self._format_image_member_to_db(image_member) try: new_values = self.db_api.image_member_update(self.context,
glance/domain/proxy.py+2 −2 modified@@ -94,9 +94,9 @@ def add(self, item): result = self.base.add(base_item) return self.helper.proxy(result) - def save(self, item): + def save(self, item, from_state=None): base_item = self.helper.unproxy(item) - result = self.base.save(base_item) + result = self.base.save(base_item, from_state=from_state) return self.helper.proxy(result) def remove(self, item):
glance/notifier.py+2 −2 modified@@ -178,8 +178,8 @@ def __init__(self, image_repo, context, notifier): item_proxy_class=ImageProxy, item_proxy_kwargs=proxy_kwargs) - def save(self, image): - super(ImageRepoProxy, self).save(image) + def save(self, image, from_state=None): + super(ImageRepoProxy, self).save(image, from_state=from_state) self.notifier.info('image.update', format_image_notification(image))
glance/quota/__init__.py+2 −2 modified@@ -96,9 +96,9 @@ def _enforce_image_property_quota(self, image): raise exception.ImagePropertyLimitExceeded(attempted=attempted, maximum=maximum) - def save(self, image): + def save(self, image, from_state=None): self._enforce_image_property_quota(image) - super(ImageRepoProxy, self).save(image) + return super(ImageRepoProxy, self).save(image, from_state=from_state) def add(self, image): self._enforce_image_property_quota(image)
glance/store/__init__.py+1 −1 modified@@ -446,7 +446,7 @@ def add(self, image): self._set_acls(image) return result - def save(self, image): + def save(self, image, from_state=None): result = super(ImageRepoProxy, self).save(image) self._set_acls(image) return result
glance/tests/unit/test_domain_proxy.py+8 −6 modified@@ -74,7 +74,7 @@ def test_add(self): self._test_method('add', 'snuff', 'enough') def test_save(self): - self._test_method('save', 'snuff', 'enough') + self._test_method('save', 'snuff', 'enough', from_state=None) def test_remove(self): self._test_method('add', None, 'flying') @@ -121,14 +121,14 @@ def test_list(self): self.assertEqual(results[i].args, tuple()) self.assertEqual(results[i].kwargs, {'a': 1}) - def _test_method_with_proxied_argument(self, name, result): + def _test_method_with_proxied_argument(self, name, result, **kwargs): self.fake_repo.result = result item = FakeProxy('snoop') method = getattr(self.proxy_repo, name) proxy_result = method(item) - self.assertEqual(self.fake_repo.args, ('snoop',)) - self.assertEqual(self.fake_repo.kwargs, {}) + self.assertEqual(('snoop',), self.fake_repo.args) + self.assertEqual(kwargs, self.fake_repo.kwargs) if result is None: self.assertTrue(proxy_result is None) @@ -145,10 +145,12 @@ def test_add_with_no_result(self): self._test_method_with_proxied_argument('add', None) def test_save(self): - self._test_method_with_proxied_argument('save', 'dog') + self._test_method_with_proxied_argument('save', 'dog', + from_state=None) def test_save_with_no_result(self): - self._test_method_with_proxied_argument('save', None) + self._test_method_with_proxied_argument('save', None, + from_state=None) def test_remove(self): self._test_method_with_proxied_argument('remove', 'dog')
glance/tests/unit/test_policy.py+1 −1 modified@@ -69,7 +69,7 @@ def add(self, image_member): def get(self, *args, **kwargs): return 'member_repo_get' - def save(self, image_member): + def save(self, image_member, from_state=None): image_member.output = 'member_repo_save' def list(self, *args, **kwargs):
glance/tests/unit/test_quota.py+4 −2 modified@@ -290,7 +290,8 @@ def test_save_image_with_image_property(self): self.image.extra_properties = {'foo': 'bar'} self.image_repo_proxy.save(self.image) - self.image_repo_mock.save.assert_called_once_with(self.base_image) + self.image_repo_mock.save.assert_called_once_with(self.base_image, + from_state=None) def test_save_image_too_many_image_properties(self): self.config(image_property_quota=1) @@ -306,7 +307,8 @@ def test_save_image_unlimited_image_properties(self): self.image.extra_properties = {'foo': 'bar'} self.image_repo_proxy.save(self.image) - self.image_repo_mock.save.assert_called_once_with(self.base_image) + self.image_repo_mock.save.assert_called_once_with(self.base_image, + from_state=None) def test_add_image_with_image_property(self): self.config(image_property_quota=1)
glance/tests/unit/test_store_image.py+1 −1 modified@@ -33,7 +33,7 @@ class ImageRepoStub(object): def add(self, image): return image - def save(self, image): + def save(self, image, from_state=None): return image
glance/tests/unit/v1/test_api.py+24 −27 modified@@ -1583,8 +1583,7 @@ def mock_store_add_to_backend_w_exception(*args, **kwargs): self.assertEqual(1, mock_store_add_to_backend.call_count) - def test_delete_during_image_upload(self): - req = unit_test_utils.get_fake_request() + def _check_delete_during_image_upload(self, is_admin=False): fixture_headers = {'x-image-meta-store': 'file', 'x-image-meta-disk-format': 'vhd', @@ -1618,30 +1617,18 @@ def mock_initiate_deletion(*args, **kwargs): mock_initiate_deletion) orig_update_image_metadata = registry.update_image_metadata - ctlr = glance.api.v1.controller.BaseController - orig_get_image_meta_or_404 = ctlr.get_image_meta_or_404 - def mock_update_image_metadata(*args, **kwargs): - - if args[2].get('status', None) == 'deleted': + data = "somedata" - # One shot. - def mock_get_image_meta_or_404(*args, **kwargs): - ret = orig_get_image_meta_or_404(*args, **kwargs) - ret['status'] = 'queued' - self.stubs.Set(ctlr, 'get_image_meta_or_404', - orig_get_image_meta_or_404) - return ret - - self.stubs.Set(ctlr, 'get_image_meta_or_404', - mock_get_image_meta_or_404) + def mock_update_image_metadata(*args, **kwargs): - req = webob.Request.blank("/images/%s" % image_id) - req.method = 'PUT' - req.headers['Content-Type'] = 'application/octet-stream' - req.body = "somedata" + if args[2].get('size', None) == len(data): + path = "/images/%s" % image_id + req = unit_test_utils.get_fake_request(path=path, + method='DELETE', + is_admin=is_admin) res = req.get_response(self.api) - self.assertEqual(res.status_int, 200) + self.assertEqual(200, res.status_int) self.stubs.Set(registry, 'update_image_metadata', orig_update_image_metadata) @@ -1651,20 +1638,30 @@ def mock_get_image_meta_or_404(*args, **kwargs): self.stubs.Set(registry, 'update_image_metadata', mock_update_image_metadata) - req = webob.Request.blank("/images/%s" % image_id) - req.method = 'DELETE' + req = unit_test_utils.get_fake_request(path="/images/%s" % image_id, + method='PUT') + req.headers['Content-Type'] = 'application/octet-stream' + req.body = data res = req.get_response(self.api) - self.assertEqual(res.status_int, 200) + self.assertEqual(412, res.status_int) + self.assertFalse(res.location) self.assertTrue(called['initiate_deletion']) - req = webob.Request.blank("/images/%s" % image_id) - req.method = 'HEAD' + req = unit_test_utils.get_fake_request(path="/images/%s" % image_id, + method='HEAD', + is_admin=True) res = req.get_response(self.api) self.assertEqual(res.status_int, 200) self.assertEqual(res.headers['x-image-meta-deleted'], 'True') self.assertEqual(res.headers['x-image-meta-status'], 'deleted') + def test_delete_during_image_upload_by_normal_user(self): + self._check_delete_during_image_upload(is_admin=False) + + def test_delete_during_image_upload_by_admin(self): + self._check_delete_during_image_upload(is_admin=True) + def test_disable_purge_props(self): """ Test the special x-glance-registry-purge-props header controls
glance/tests/unit/v2/test_image_data_resource.py+14 −10 modified@@ -79,7 +79,7 @@ def get(self, image_id): else: return self.result - def save(self, image): + def save(self, image, from_state=None): self.saved_image = image @@ -180,17 +180,21 @@ def test_upload_invalid(self): request, unit_test_utils.UUID1, 'YYYY', 4) def test_upload_non_existent_image_during_save_initiates_deletion(self): - def fake_save(self): + def fake_save_not_found(self): raise exception.NotFound() - request = unit_test_utils.get_fake_request() - image = FakeImage('abcd', locations=['http://example.com/image']) - self.image_repo.result = image - self.image_repo.save = fake_save - image.delete = mock.Mock() - self.assertRaises(webob.exc.HTTPGone, self.controller.upload, - request, str(uuid.uuid4()), 'ABC', 3) - self.assertTrue(image.delete.called) + def fake_save_conflict(self): + raise exception.Conflict() + + for fun in [fake_save_not_found, fake_save_conflict]: + request = unit_test_utils.get_fake_request() + image = FakeImage('abcd', locations=['http://example.com/image']) + self.image_repo.result = image + self.image_repo.save = fun + image.delete = mock.Mock() + self.assertRaises(webob.exc.HTTPGone, self.controller.upload, + request, str(uuid.uuid4()), 'ABC', 3) + self.assertTrue(image.delete.called) def test_upload_non_existent_image_before_save(self): request = unit_test_utils.get_fake_request()
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
14- bugs.launchpad.net/glance/+bug/1398830nvdExploitWEB
- github.com/advisories/GHSA-j4mh-9wq6-8rg6ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2014-9623ghsaADVISORY
- rhn.redhat.com/errata/RHSA-2015-0644.htmlnvdWEB
- rhn.redhat.com/errata/RHSA-2015-0837.htmlnvdWEB
- rhn.redhat.com/errata/RHSA-2015-0838.htmlnvdWEB
- secunia.com/advisories/62165nvdWEB
- www.openwall.com/lists/oss-security/2015/01/18/4nvdWEB
- www.oracle.com/technetwork/topics/security/bulletinapr2015-2511959.htmlnvdWEB
- bugs.launchpad.net/glance/+bug/1383973nvdWEB
- github.com/openstack/glance/commit/0dc8fbb3479a53c5bba8475d14f4c7206904c5eaghsaWEB
- github.com/openstack/glance/commit/7d5d8657fd70b20518610b3c6f8e41e16c72fa31ghsaWEB
- github.com/openstack/glance/commit/f1260cc771ee068651aa62b972bef49d9af81eb0ghsaWEB
- security.openstack.org/ossa/OSSA-2015-003.htmlnvdWEB
News mentions
0No linked articles in our index yet.