File 0001-stable-only-cve-Check-VMDK-create-type-against-an-al.patch of Package openstack-nova
From 0d9b0e568fe0fabce2cb814edf1a57a7707e2eed Mon Sep 17 00:00:00 2001
From: Dan Smith <dansmith@redhat.com>
Date: Thu, 10 Nov 2022 09:55:48 -0800
Subject: [PATCH] [stable-only][cve] Check VMDK create-type against an allowed
list
Trivial conflicts on xena only in:
nova/conf/compute.py
NOTE(sbauza): Stable policy allows us to proactively merge a backport without waiting for the parent patch to be merged (exception to rule #4 in [1]. Marking [stable-only] in order to silence nova-tox-validate-backport
[1] https://docs.openstack.org/project-team-guide/stable-branches.html#appropriate-fixes
Related-Bug: #1996188
Change-Id: I5a399f1d3d702bfb76c067893e9c924904c8c360
(cherry picked from commit 867c4dd893ea7211e89b78b22b8da920a74622ff)
For Rocky backport, changed qemu_img_info mock in test_fetch_checks_vmdk_rules.
Changed qemu_img_info() so it calls qemu-img with output='json' when called
from fetch_to_raw() so format-specific fields are parsed.
---
nova/conf/compute.py | 9 ++++++
nova/tests/unit/virt/libvirt/test_utils.py | 2 +-
nova/tests/unit/virt/test_images.py | 47 ++++++++++++++++++++++++++++++
nova/virt/images.py | 42 ++++++++++++++++++++++++--
4 files changed, 96 insertions(+), 4 deletions(-)
diff --git a/nova/conf/compute.py b/nova/conf/compute.py
index 5436374..44edaf4 100644
--- a/nova/conf/compute.py
+++ b/nova/conf/compute.py
@@ -621,6 +621,15 @@ Possible values:
]
compute_group_opts = [
+ cfg.ListOpt('vmdk_allowed_types',
+ default=['streamOptimized', 'monolithicSparse'],
+ help="""
+A list of strings describing allowed VMDK "create-type" subformats
+that will be allowed. This is recommended to only include
+single-file-with-sparse-header variants to avoid potential host file
+exposure due to processing named extents. If this list is empty, then no
+form of VMDK image will be allowed.
+"""),
cfg.IntOpt('consecutive_build_service_disable_threshold',
default=10,
help="""
diff --git a/nova/tests/unit/virt/libvirt/test_utils.py b/nova/tests/unit/virt/libvirt/test_utils.py
index 898d517..1e3293c 100644
--- a/nova/tests/unit/virt/libvirt/test_utils.py
+++ b/nova/tests/unit/virt/libvirt/test_utils.py
@@ -649,7 +649,7 @@ disk size: 4.4M
def fake_rm_on_error(path, remove=None):
self.executes.append(('rm', '-f', path))
- def fake_qemu_img_info(path):
+ def fake_qemu_img_info(path, use_json=False):
class FakeImgInfo(object):
pass
diff --git a/nova/tests/unit/virt/test_images.py b/nova/tests/unit/virt/test_images.py
index 2baf445..e448d7a 100644
--- a/nova/tests/unit/virt/test_images.py
+++ b/nova/tests/unit/virt/test_images.py
@@ -16,6 +16,8 @@ import os
import mock
from oslo_concurrency import processutils
+from oslo_serialization import jsonutils
+from oslo_utils import imageutils
import six
from nova import exception
@@ -120,3 +122,48 @@ class QemuTestCase(test.NoDBTestCase):
expected = ('qemu-img', 'convert', '-t', 'writethrough',
'-O', 'out_format', '-f', 'in_format', 'source', 'dest')
self.assertTupleEqual(expected, mock_execute.call_args[0])
+
+ def test_convert_image_vmdk_allowed_list_checking(self):
+ info = {'format': 'vmdk',
+ 'format-specific': {
+ 'type': 'vmdk',
+ 'data': {
+ 'create-type': 'monolithicFlat',
+ }}}
+
+ # If the format is not in the allowed list, we should get an error
+ self.assertRaises(exception.ImageUnacceptable,
+ images.check_vmdk_image, 'foo',
+ imageutils.QemuImgInfo(jsonutils.dumps(info),
+ format='json'))
+
+ # With the format in the allowed list, no error
+ self.flags(vmdk_allowed_types=['streamOptimized', 'monolithicFlat',
+ 'monolithicSparse'],
+ group='compute')
+ images.check_vmdk_image('foo',
+ imageutils.QemuImgInfo(jsonutils.dumps(info),
+ format='json'))
+
+ # With an empty list, allow nothing
+ self.flags(vmdk_allowed_types=[], group='compute')
+ self.assertRaises(exception.ImageUnacceptable,
+ images.check_vmdk_image, 'foo',
+ imageutils.QemuImgInfo(jsonutils.dumps(info),
+ format='json'))
+
+ @mock.patch.object(images, 'fetch')
+ @mock.patch.object(images, 'qemu_img_info')
+ def test_fetch_checks_vmdk_rules(self, mock_info, mock_fetch):
+ info = {'format': 'vmdk',
+ 'format-specific': {
+ 'type': 'vmdk',
+ 'data': {
+ 'create-type': 'monolithicFlat',
+ }}}
+ mock_info.return_value = imageutils.QemuImgInfo(jsonutils.dumps(info),
+ format='json')
+ with mock.patch('os.path.exists', return_value=True):
+ e = self.assertRaises(exception.ImageUnacceptable,
+ images.fetch_to_raw, None, 'foo', 'anypath')
+ self.assertIn('Invalid VMDK create-type specified', str(e))
diff --git a/nova/virt/images.py b/nova/virt/images.py
index dbaa4e7..4778b05 100644
--- a/nova/virt/images.py
+++ b/nova/virt/images.py
@@ -50,7 +50,7 @@ QEMU_VERSION = None
QEMU_VERSION_REQ_SHARED = 2010000
-def qemu_img_info(path, format=None):
+def qemu_img_info(path, format=None, use_json=False):
"""Return an object containing the parsed output from qemu-img info."""
# TODO(mikal): this code should not be referring to a libvirt specific
# flag.
@@ -65,6 +65,8 @@ def qemu_img_info(path, format=None):
path = os.path.join(path, "root.hds")
cmd = ('env', 'LC_ALL=C', 'LANG=C', 'qemu-img', 'info', path)
+ if use_json:
+ cmd = cmd + ('--output=json',)
if format is not None:
cmd = cmd + ('-f', format)
# Check to see if the qemu version is >= 2.10 because if so, we need
@@ -92,7 +94,10 @@ def qemu_img_info(path, format=None):
{'path': path, 'error': err})
raise exception.InvalidDiskInfo(reason=msg)
- return imageutils.QemuImgInfo(out)
+ if use_json:
+ return imageutils.QemuImgInfo(out, format='json')
+ else:
+ return imageutils.QemuImgInfo(out)
def convert_image(source, dest, in_format, out_format, run_as_root=False):
@@ -141,12 +146,40 @@ def get_info(context, image_href):
return IMAGE_API.get(context, image_href)
+def check_vmdk_image(image_id, data):
+ # Check some rules about VMDK files. Specifically we want to make
+ # sure that the "create-type" of the image is one that we allow.
+ # Some types of VMDK files can reference files outside the disk
+ # image and we do not want to allow those for obvious reasons.
+
+ types = CONF.compute.vmdk_allowed_types
+
+ if not len(types):
+ LOG.warning('Refusing to allow VMDK image as vmdk_allowed_'
+ 'types is empty')
+ msg = _('Invalid VMDK create-type specified')
+ raise exception.ImageUnacceptable(image_id=image_id, reason=msg)
+
+ try:
+ create_type = data.format_specific['data']['create-type']
+ except KeyError:
+ msg = _('Unable to determine VMDK create-type')
+ raise exception.ImageUnacceptable(image_id=image_id, reason=msg)
+
+ if create_type not in CONF.compute.vmdk_allowed_types:
+ LOG.warning('Refusing to process VMDK file with create-type of %r '
+ 'which is not in allowed set of: %s', create_type,
+ ','.join(CONF.compute.vmdk_allowed_types))
+ msg = _('Invalid VMDK create-type specified')
+ raise exception.ImageUnacceptable(image_id=image_id, reason=msg)
+
+
def fetch_to_raw(context, image_href, path, trusted_certs=None):
path_tmp = "%s.part" % path
fetch(context, image_href, path_tmp, trusted_certs)
with fileutils.remove_path_on_error(path_tmp):
- data = qemu_img_info(path_tmp)
+ data = qemu_img_info(path_tmp, use_json=True)
fmt = data.file_format
if fmt is None:
@@ -160,6 +193,9 @@ def fetch_to_raw(context, image_href, path, trusted_certs=None):
reason=(_("fmt=%(fmt)s backed by: %(backing_file)s") %
{'fmt': fmt, 'backing_file': backing_file}))
+ if fmt == 'vmdk':
+ check_vmdk_image(image_href, data)
+
if fmt != "raw" and CONF.force_raw_images:
staged = "%s.converted" % path
LOG.debug("%s was %s, converting to raw", image_href, fmt)
--
2.7.4