Files
nova/nova/virt/images.py
Dan Smith da352edceb Check images with format_inspector for safety
It has been asserted that we should not be calling qemu-img info
on untrusted files. That means we need to know if they have a
backing_file, data_file or other unsafe configuration *before* we use
qemu-img to probe or convert them.

This grafts glance's format_inspector module into nova/images so we
can use it to check the file early for safety. The expectation is that
this will be moved to oslo.utils (or something) later and thus we will
just delete the file from nova and change our import when that happens.

NOTE: This includes whitespace changes from the glance version of
format_inspector.py because of autopep8 demands.

Change-Id: Iaefbe41b4c4bf0cf95d8f621653fdf65062aaa59
Closes-Bug: #2059809
(cherry picked from commit 9cdce71594)
(cherry picked from commit f07fa55fd8)
(cherry picked from commit 0acf5ee7b5)
(cherry picked from commit 67e5376dd6)
2024-07-06 11:53:39 +02:00

240 lines
9.6 KiB
Python

# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
# Copyright (c) 2010 Citrix Systems, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Handling of VM disk images.
"""
import os
from oslo_concurrency import processutils
from oslo_log import log as logging
from oslo_utils import fileutils
from oslo_utils import imageutils
from nova.compute import utils as compute_utils
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.qemu
LOG = logging.getLogger(__name__)
CONF = nova.conf.CONF
IMAGE_API = glance.API()
def qemu_img_info(path, format=None):
"""Return an object containing the parsed output from qemu-img info."""
if not os.path.exists(path) and not path.startswith('rbd:'):
raise exception.DiskNotFound(location=path)
info = nova.privsep.qemu.unprivileged_qemu_img_info(path, format=format)
return imageutils.QemuImgInfo(info, format='json')
def privileged_qemu_img_info(path, format=None, output_format='json'):
"""Return an object containing the parsed output from qemu-img info."""
if not os.path.exists(path) and not path.startswith('rbd:'):
raise exception.DiskNotFound(location=path)
info = nova.privsep.qemu.privileged_qemu_img_info(path, format=format)
return imageutils.QemuImgInfo(info, format='json')
def convert_image(source, dest, in_format, out_format, run_as_root=False,
compress=False):
"""Convert image to other format."""
if in_format is None:
raise RuntimeError("convert_image without input format is a security"
" risk")
_convert_image(source, dest, in_format, out_format, run_as_root,
compress=compress)
def convert_image_unsafe(source, dest, out_format, run_as_root=False):
"""Convert image to other format, doing unsafe automatic input format
detection. Do not call this function.
"""
# NOTE: there is only 1 caller of this function:
# imagebackend.Lvm.create_image. It is not easy to fix that without a
# larger refactor, so for the moment it has been manually audited and
# allowed to continue. Remove this function when Lvm.create_image has
# been fixed.
_convert_image(source, dest, None, out_format, run_as_root)
def _convert_image(source, dest, in_format, out_format, run_as_root,
compress=False):
try:
with compute_utils.disk_ops_semaphore:
if not run_as_root:
nova.privsep.qemu.unprivileged_convert_image(
source, dest, in_format, out_format, CONF.instances_path,
compress)
else:
nova.privsep.qemu.convert_image(
source, dest, in_format, out_format, CONF.instances_path,
compress)
except processutils.ProcessExecutionError as exp:
msg = (_("Unable to convert image to %(format)s: %(exp)s") %
{'format': out_format, 'exp': exp})
raise exception.ImageUnacceptable(image_id=source, reason=msg)
def fetch(context, image_href, path, trusted_certs=None):
with fileutils.remove_path_on_error(path):
with compute_utils.disk_ops_semaphore:
IMAGE_API.download(context, image_href, dest_path=path,
trusted_certs=trusted_certs)
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 do_image_deep_inspection(img, image_href, path):
disk_format = img['disk_format']
try:
# NOTE(danms): Use our own cautious inspector module to make sure
# the image file passes safety checks.
# See https://bugs.launchpad.net/nova/+bug/2059809 for details.
inspector_cls = format_inspector.get_inspector(disk_format)
if not inspector_cls.from_file(path).safety_check():
raise exception.ImageUnacceptable(
image_id=image_href,
reason=(_('Image does not pass safety check')))
except format_inspector.ImageFormatError:
# If the inspector we chose based on the image's metadata does not
# think the image is the proper format, we refuse to use it.
raise exception.ImageUnacceptable(
image_id=image_href,
reason=_('Image content does not match disk_format'))
except AttributeError:
# No inspector was found
LOG.warning('Unable to perform deep image inspection on type %r',
img['disk_format'])
if disk_format in ('ami', 'aki', 'ari'):
# A lot of things can be in a UEC, although it is typically a raw
# filesystem. We really have nothing we can do other than treat it
# like a 'raw', which is what qemu-img will detect a filesystem as
# anyway. If someone puts a qcow2 inside, we should fail because
# we won't do our inspection.
disk_format = 'raw'
else:
raise exception.ImageUnacceptable(
image_id=image_href,
reason=_('Image not in a supported format'))
return disk_format
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):
if not CONF.workarounds.disable_deep_image_inspection:
# If we're doing deep inspection, we take the determined format
# from it.
img = IMAGE_API.get(context, image_href)
force_format = do_image_deep_inspection(img, image_href, path_tmp)
else:
force_format = None
# Only run qemu-img after we have done deep inspection (if enabled).
# If it was not enabled, we will let it detect the format.
data = qemu_img_info(path_tmp, format=force_format)
fmt = data.file_format
if fmt is None:
raise exception.ImageUnacceptable(
reason=_("'qemu-img info' parsing failed."),
image_id=image_href)
backing_file = data.backing_file
if backing_file is not None:
raise exception.ImageUnacceptable(image_id=image_href,
reason=(_("fmt=%(fmt)s backed by: %(backing_file)s") %
{'fmt': fmt, 'backing_file': backing_file}))
try:
data_file = data.format_specific['data']['data-file']
except (KeyError, TypeError, AttributeError):
data_file = None
if data_file is not None:
raise exception.ImageUnacceptable(image_id=image_href,
reason=(_("fmt=%(fmt)s has data-file: %(data_file)s") %
{'fmt': fmt, 'data_file': data_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)
with fileutils.remove_path_on_error(staged):
try:
convert_image(path_tmp, staged, fmt, 'raw')
except exception.ImageUnacceptable as exp:
# re-raise to include image_href
raise exception.ImageUnacceptable(image_id=image_href,
reason=_("Unable to convert image to raw: %(exp)s")
% {'exp': exp})
os.unlink(path_tmp)
data = qemu_img_info(staged)
if data.file_format != "raw":
raise exception.ImageUnacceptable(image_id=image_href,
reason=_("Converted to raw, but format is now %s") %
data.file_format)
os.rename(staged, path)
else:
os.rename(path_tmp, path)