Merge "Additional qemu safety checking on base images" into unmaintained/yoga

This commit is contained in:
Zuul 2024-08-13 17:48:49 +00:00 committed by Gerrit Code Review
commit ef98d9d293
6 changed files with 194 additions and 17 deletions

View File

@ -368,6 +368,23 @@ class QcowInspector(FileInspector):
not self.has_unknown_features)
class QEDInspector(FileInspector):
def __init__(self, tracing=False):
super().__init__(tracing)
self.new_region('header', CaptureRegion(0, 512))
@property
def format_match(self):
if not self.region('header').complete:
return False
return self.region('header').data.startswith(b'QED\x00')
def safety_check(self):
# QED format is not supported by anyone, but we want to detect it
# and mark it as just always unsafe.
return False
# The VHD (or VPC as QEMU calls it) format consists of a big-endian
# 512-byte "footer" at the beginning of the file with various
# information, most of which does not matter to us:
@ -871,19 +888,52 @@ class InfoWrapper(object):
self._source.close()
ALL_FORMATS = {
'raw': FileInspector,
'qcow2': QcowInspector,
'vhd': VHDInspector,
'vhdx': VHDXInspector,
'vmdk': VMDKInspector,
'vdi': VDIInspector,
'qed': QEDInspector,
}
def get_inspector(format_name):
"""Returns a FormatInspector class based on the given name.
:param format_name: The name of the disk_format (raw, qcow2, etc).
:returns: A FormatInspector or None if unsupported.
"""
formats = {
'raw': FileInspector,
'qcow2': QcowInspector,
'vhd': VHDInspector,
'vhdx': VHDXInspector,
'vmdk': VMDKInspector,
'vdi': VDIInspector,
}
return formats.get(format_name)
return ALL_FORMATS.get(format_name)
def detect_file_format(filename):
"""Attempts to detect the format of a file.
This runs through a file one time, running all the known inspectors in
parallel. It stops reading the file once one of them matches or all of
them are sure they don't match.
Returns the FileInspector that matched, if any. None if 'raw'.
"""
inspectors = {k: v() for k, v in ALL_FORMATS.items()}
with open(filename, 'rb') as f:
for chunk in chunked_reader(f):
for format, inspector in list(inspectors.items()):
try:
inspector.eat_chunk(chunk)
except ImageFormatError:
# No match, so stop considering this format
inspectors.pop(format)
continue
if (inspector.format_match and inspector.complete and
format != 'raw'):
# First complete match (other than raw) wins
return inspector
if all(i.complete for i in inspectors.values()):
# If all the inspectors are sure they are not a match, avoid
# reading to the end of the file to settle on 'raw'.
break
return inspectors['raw']

View File

@ -13793,10 +13793,11 @@ class LibvirtConnTestCase(test.NoDBTestCase,
'/fake/instance/dir', disk_info)
self.assertFalse(mock_fetch_image.called)
@mock.patch('nova.image.format_inspector.detect_file_format')
@mock.patch('nova.privsep.path.utime')
@mock.patch('nova.virt.libvirt.utils.create_cow_image')
def test_create_images_and_backing_ephemeral_gets_created(
self, mock_create_cow_image, mock_utime):
self, mock_create_cow_image, mock_utime, mock_detect):
drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False)
base_dir = os.path.join(CONF.instances_path,
@ -15532,11 +15533,13 @@ class LibvirtConnTestCase(test.NoDBTestCase,
fake_mkfs.assert_has_calls([mock.call('ext4', '/dev/something',
'myVol')])
@mock.patch('nova.image.format_inspector.detect_file_format')
@mock.patch('nova.privsep.path.utime')
@mock.patch('nova.virt.libvirt.utils.fetch_image')
@mock.patch('nova.virt.libvirt.utils.create_cow_image')
def test_create_ephemeral_specified_fs_not_valid(
self, mock_create_cow_image, mock_fetch_image, mock_utime):
self, mock_create_cow_image, mock_fetch_image, mock_utime,
mock_detect):
CONF.set_override('default_ephemeral_format', 'ext4')
ephemerals = [{'device_type': 'disk',
'disk_bus': 'virtio',

View File

@ -522,13 +522,15 @@ class Qcow2TestCase(_ImageTestCase, test.NoDBTestCase):
mock_exists.assert_has_calls(exist_calls)
@mock.patch('nova.image.format_inspector.detect_file_format')
@mock.patch.object(imagebackend.utils, 'synchronized')
@mock.patch('nova.virt.libvirt.utils.create_cow_image')
@mock.patch.object(os.path, 'exists', side_effect=[])
@mock.patch.object(imagebackend.Image, 'verify_base_size')
@mock.patch('nova.privsep.path.utime')
def test_create_image(
self, mock_utime, mock_verify, mock_exist, mock_create, mock_sync
self, mock_utime, mock_verify, mock_exist, mock_create, mock_sync,
mock_detect_format
):
mock_sync.side_effect = lambda *a, **kw: self._fake_deco
fn = mock.MagicMock()
@ -549,7 +551,10 @@ class Qcow2TestCase(_ImageTestCase, test.NoDBTestCase):
mock_exist.assert_has_calls(exist_calls)
self.assertTrue(mock_sync.called)
mock_utime.assert_called()
mock_detect_format.assert_called_once()
mock_detect_format.return_value.safety_check.assert_called_once_with()
@mock.patch('nova.image.format_inspector.detect_file_format')
@mock.patch.object(imagebackend.utils, 'synchronized')
@mock.patch('nova.virt.libvirt.utils.create_cow_image')
@mock.patch.object(imagebackend.disk, 'extend')
@ -557,7 +562,8 @@ class Qcow2TestCase(_ImageTestCase, test.NoDBTestCase):
@mock.patch.object(imagebackend.Qcow2, 'get_disk_size')
@mock.patch('nova.privsep.path.utime')
def test_create_image_too_small(self, mock_utime, mock_get, mock_exist,
mock_extend, mock_create, mock_sync):
mock_extend, mock_create, mock_sync,
mock_detect_format):
mock_sync.side_effect = lambda *a, **kw: self._fake_deco
mock_get.return_value = self.SIZE
fn = mock.MagicMock()
@ -574,7 +580,9 @@ class Qcow2TestCase(_ImageTestCase, test.NoDBTestCase):
self.assertTrue(mock_sync.called)
self.assertFalse(mock_create.called)
self.assertFalse(mock_extend.called)
mock_detect_format.assert_called_once()
@mock.patch('nova.image.format_inspector.detect_file_format')
@mock.patch.object(imagebackend.utils, 'synchronized')
@mock.patch('nova.virt.libvirt.utils.create_cow_image')
@mock.patch('nova.virt.libvirt.utils.get_disk_backing_file')
@ -586,7 +594,8 @@ class Qcow2TestCase(_ImageTestCase, test.NoDBTestCase):
def test_generate_resized_backing_files(self, mock_utime, mock_copy,
mock_verify, mock_exist,
mock_extend, mock_get,
mock_create, mock_sync):
mock_create, mock_sync,
mock_detect_format):
mock_sync.side_effect = lambda *a, **kw: self._fake_deco
mock_get.return_value = self.QCOW2_BASE
fn = mock.MagicMock()
@ -613,7 +622,9 @@ class Qcow2TestCase(_ImageTestCase, test.NoDBTestCase):
self.assertTrue(mock_sync.called)
self.assertFalse(mock_create.called)
mock_utime.assert_called()
mock_detect_format.assert_called_once()
@mock.patch('nova.image.format_inspector.detect_file_format')
@mock.patch.object(imagebackend.utils, 'synchronized')
@mock.patch('nova.virt.libvirt.utils.create_cow_image')
@mock.patch('nova.virt.libvirt.utils.get_disk_backing_file')
@ -624,7 +635,8 @@ class Qcow2TestCase(_ImageTestCase, test.NoDBTestCase):
def test_qcow2_exists_and_has_no_backing_file(self, mock_utime,
mock_verify, mock_exist,
mock_extend, mock_get,
mock_create, mock_sync):
mock_create, mock_sync,
mock_detect_format):
mock_sync.side_effect = lambda *a, **kw: self._fake_deco
mock_get.return_value = None
fn = mock.MagicMock()
@ -645,6 +657,31 @@ class Qcow2TestCase(_ImageTestCase, test.NoDBTestCase):
self.assertTrue(mock_sync.called)
self.assertFalse(mock_create.called)
self.assertFalse(mock_extend.called)
mock_detect_format.assert_called_once()
@mock.patch('nova.image.format_inspector.detect_file_format')
@mock.patch.object(imagebackend.utils, 'synchronized')
@mock.patch('nova.virt.libvirt.utils.create_image')
@mock.patch('nova.virt.libvirt.utils.get_disk_backing_file')
@mock.patch.object(imagebackend.disk, 'extend')
@mock.patch.object(os.path, 'exists', side_effect=[])
@mock.patch.object(imagebackend.Image, 'verify_base_size')
def test_qcow2_exists_and_fails_safety_check(self,
mock_verify, mock_exist,
mock_extend, mock_get,
mock_create, mock_sync,
mock_detect_format):
mock_detect_format.return_value.safety_check.return_value = False
mock_sync.side_effect = lambda *a, **kw: self._fake_deco
mock_get.return_value = None
fn = mock.MagicMock()
mock_exist.side_effect = [False, True, False, True, True]
image = self.image_class(self.INSTANCE, self.NAME)
self.assertRaises(exception.InvalidDiskInfo,
image.create_image, fn, self.TEMPLATE_PATH,
self.SIZE)
mock_verify.assert_not_called()
def test_resolve_driver_format(self):
image = self.image_class(self.INSTANCE, self.NAME)

View File

@ -117,11 +117,27 @@ class LibvirtUtilsTestCase(test.NoDBTestCase):
@mock.patch('os.path.exists', return_value=True)
@mock.patch('oslo_concurrency.processutils.execute')
@mock.patch('nova.virt.images.qemu_img_info')
def test_create_cow_image(self, mock_info, mock_execute, mock_exists):
@mock.patch('nova.image.format_inspector.detect_file_format')
def _test_create_cow_image(
self, mock_detect, mock_info, mock_execute,
mock_exists, backing_file=None, safety_check=True
):
if isinstance(backing_file, dict):
backing_info = backing_file
backing_file = backing_info.pop('file', None)
else:
backing_info = {}
backing_backing_file = backing_info.pop('backing_file', None)
mock_execute.return_value = ('stdout', None)
mock_info.return_value = mock.Mock(
file_format=mock.sentinel.backing_fmt,
cluster_size=mock.sentinel.cluster_size)
cluster_size=mock.sentinel.cluster_size,
backing_file=backing_backing_file,
format_specific=backing_info)
mock_detect.return_value.safety_check.return_value = safety_check
libvirt_utils.create_cow_image(mock.sentinel.backing_path,
mock.sentinel.new_path)
mock_info.assert_called_once_with(mock.sentinel.backing_path)
@ -131,6 +147,33 @@ class LibvirtUtilsTestCase(test.NoDBTestCase):
mock.sentinel.backing_path, mock.sentinel.backing_fmt,
mock.sentinel.cluster_size),
mock.sentinel.new_path)])
if backing_file:
mock_detect.return_value.safety_check.assert_called_once_with()
def test_create_image_qcow2(self):
self._test_create_cow_image()
def test_create_image_backing_file(self):
self._test_create_cow_image(
backing_file=mock.sentinel.backing_file
)
def test_create_image_base_has_backing_file(self):
self.assertRaises(
exception.InvalidDiskInfo,
self._test_create_cow_image,
backing_file={'file': mock.sentinel.backing_file,
'backing_file': mock.sentinel.backing_backing_file},
)
def test_create_image_base_has_data_file(self):
self.assertRaises(
exception.InvalidDiskInfo,
self._test_create_cow_image,
backing_file={'file': mock.sentinel.backing_file,
'backing_file': mock.sentinel.backing_backing_file,
'data': {'data-file': mock.sentinel.data_file}},
)
@ddt.unpack
@ddt.data({'fs_type': 'some_fs_type',

View File

@ -34,6 +34,7 @@ from oslo_utils import units
import nova.conf
from nova import exception
from nova.i18n import _
from nova.image import format_inspector
from nova.image import glance
import nova.privsep.libvirt
import nova.privsep.path
@ -637,6 +638,20 @@ class Qcow2(Image):
if not os.path.exists(base):
prepare_template(target=base, *args, **kwargs)
# NOTE(danms): We need to perform safety checks on the base image
# before we inspect it for other attributes. We do this each time
# because additional safety checks could have been added since we
# downloaded the image.
if not CONF.workarounds.disable_deep_image_inspection:
inspector = format_inspector.detect_file_format(base)
if not inspector.safety_check():
LOG.warning('Base image %s failed safety check', base)
# NOTE(danms): This is the same exception as would be raised
# by qemu_img_info() if the disk format was unreadable or
# otherwise unsuitable.
raise exception.InvalidDiskInfo(
reason=_('Base image failed safety check'))
# NOTE(ankit): Update the mtime of the base file so the image
# cache manager knows it is in use.
_update_utime_ignore_eacces(base)

View File

@ -34,6 +34,7 @@ import nova.conf
from nova import context as nova_context
from nova import exception
from nova.i18n import _
from nova.image import format_inspector
from nova import objects
from nova.objects import fields as obj_fields
import nova.privsep.fs
@ -139,7 +140,35 @@ def create_cow_image(
base_cmd = ['qemu-img', 'create', '-f', 'qcow2']
cow_opts = []
if backing_file:
# NOTE(danms): We need to perform safety checks on the base image
# before we inspect it for other attributes. We do this each time
# because additional safety checks could have been added since we
# downloaded the image.
if not CONF.workarounds.disable_deep_image_inspection:
inspector = format_inspector.detect_file_format(backing_file)
if not inspector.safety_check():
LOG.warning('Base image %s failed safety check', backing_file)
# NOTE(danms): This is the same exception as would be raised
# by qemu_img_info() if the disk format was unreadable or
# otherwise unsuitable.
raise exception.InvalidDiskInfo(
reason=_('Base image failed safety check'))
base_details = images.qemu_img_info(backing_file)
if base_details.backing_file is not None:
LOG.warning('Base image %s failed safety check', backing_file)
raise exception.InvalidDiskInfo(
reason=_('Base image failed safety check'))
try:
data_file = base_details.format_specific['data']['data-file']
except (KeyError, TypeError, AttributeError):
data_file = None
if data_file is not None:
LOG.warning('Base image %s failed safety check', backing_file)
raise exception.InvalidDiskInfo(
reason=_('Base image failed safety check'))
cow_opts += ['backing_file=%s' % backing_file]
cow_opts += ['backing_fmt=%s' % base_details.file_format]
else: