Skip to content

Commit 85a74c3

Browse files
Zuulopenstack-gerrit
authored andcommitted
Merge "Reject unsafe qcow and vmdk files" into stable/2023.1
2 parents 30fa701 + c1c54ab commit 85a74c3

File tree

2 files changed

+87
-14
lines changed

2 files changed

+87
-14
lines changed

glance/async_/flows/plugins/image_conversion.py

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from taskflow import task
2626

2727
from glance.async_ import utils
28+
from glance.common import format_inspector
2829
from glance.i18n import _, _LI
2930

3031
LOG = logging.getLogger(__name__)
@@ -87,8 +88,40 @@ def _execute(self, action, file_path, **kwargs):
8788
'target': target_format}
8889
self.dest_path = dest_path
8990

91+
source_format = action.image_disk_format
92+
inspector_cls = format_inspector.get_inspector(source_format)
93+
if not inspector_cls:
94+
# We cannot convert from disk_format types that qemu-img doesn't
95+
# support (like iso, ploop, etc). The ones it supports overlaps
96+
# with the ones we have inspectors for, so reject conversion for
97+
# any format we don't have an inspector for.
98+
raise RuntimeError(
99+
'Unable to convert from format %s' % source_format)
100+
101+
# Use our own cautious inspector module (if we have one for this
102+
# format) to make sure a file is the format the submitter claimed
103+
# it is and that it passes some basic safety checks _before_ we run
104+
# qemu-img on it.
105+
# See https://bugs.launchpad.net/nova/+bug/2059809 for details.
106+
try:
107+
inspector = inspector_cls.from_file(src_path)
108+
if not inspector.safety_check():
109+
LOG.error('Image failed %s safety check; aborting conversion',
110+
source_format)
111+
raise RuntimeError('Image has disallowed configuration')
112+
except RuntimeError:
113+
raise
114+
except format_inspector.ImageFormatError as e:
115+
LOG.error('Image claimed to be %s format failed format '
116+
'inspection: %s', source_format, e)
117+
raise RuntimeError('Image format detection failed')
118+
except Exception as e:
119+
LOG.exception('Unknown error inspecting image format: %s', e)
120+
raise RuntimeError('Unable to inspect image')
121+
90122
try:
91123
stdout, stderr = putils.trycmd("qemu-img", "info",
124+
"-f", source_format,
92125
"--output=json",
93126
src_path,
94127
prlimit=utils.QEMU_IMG_PROC_LIMITS,
@@ -105,13 +138,10 @@ def _execute(self, action, file_path, **kwargs):
105138
raise RuntimeError(stderr)
106139

107140
metadata = json.loads(stdout)
108-
try:
109-
source_format = metadata['format']
110-
except KeyError:
111-
msg = ("Failed to do introspection as part of image "
112-
"conversion for %(iid)s: Source format not reported")
113-
LOG.error(msg, {'iid': self.image_id})
114-
raise RuntimeError(msg)
141+
if metadata.get('format') != source_format:
142+
LOG.error('Image claiming to be %s reported as %s by qemu-img',
143+
source_format, metadata.get('format', 'unknown'))
144+
raise RuntimeError('Image metadata disagrees about format')
115145

116146
virtual_size = metadata.get('virtual-size', 0)
117147
action.set_image_attribute(virtual_size=virtual_size)

glance/tests/unit/async_/flows/plugins/test_image_conversion.py

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# License for the specific language governing permissions and limitations
1414
# under the License.
1515

16+
import fixtures
1617
import json
1718
import os
1819
from unittest import mock
@@ -24,6 +25,7 @@
2425
import glance.async_.flows.api_image_import as import_flow
2526
import glance.async_.flows.plugins.image_conversion as image_conversion
2627
from glance.async_ import utils as async_utils
28+
from glance.common import format_inspector
2729
from glance.common import utils
2830
from glance import domain
2931
from glance import gateway
@@ -90,6 +92,11 @@ def setUp(self):
9092
self.image_id,
9193
self.task.task_id)
9294

95+
self.inspector_mock = mock.MagicMock()
96+
self.useFixture(fixtures.MockPatch('glance.common.format_inspector.'
97+
'get_inspector',
98+
self.inspector_mock))
99+
93100
@mock.patch.object(os, 'stat')
94101
@mock.patch.object(os, 'remove')
95102
def test_image_convert_success(self, mock_os_remove, mock_os_stat):
@@ -104,7 +111,7 @@ def test_image_convert_success(self, mock_os_remove, mock_os_stat):
104111
image = mock.MagicMock(image_id=self.image_id, virtual_size=None,
105112
extra_properties={
106113
'os_glance_import_task': self.task.task_id},
107-
disk_format='qcow2')
114+
disk_format='raw')
108115
self.img_repo.get.return_value = image
109116

110117
with mock.patch.object(processutils, 'execute') as exc_mock:
@@ -126,7 +133,7 @@ def test_image_convert_success(self, mock_os_remove, mock_os_stat):
126133
self.assertEqual(456, image.virtual_size)
127134
self.assertEqual(123, image.size)
128135

129-
def _setup_image_convert_info_fail(self):
136+
def _setup_image_convert_info_fail(self, disk_format='qcow2'):
130137
image_convert = image_conversion._ConvertImage(self.context,
131138
self.task.task_id,
132139
self.task_type,
@@ -136,7 +143,7 @@ def _setup_image_convert_info_fail(self):
136143
image = mock.MagicMock(image_id=self.image_id, virtual_size=None,
137144
extra_properties={
138145
'os_glance_import_task': self.task.task_id},
139-
disk_format='qcow2')
146+
disk_format=disk_format)
140147
self.img_repo.get.return_value = image
141148
return image_convert
142149

@@ -148,6 +155,7 @@ def test_image_convert_fails_inspection(self):
148155
convert.execute, 'file:///test/path.raw')
149156
exc_mock.assert_called_once_with(
150157
'qemu-img', 'info',
158+
'-f', 'qcow2',
151159
'--output=json',
152160
'/test/path.raw',
153161
prlimit=async_utils.QEMU_IMG_PROC_LIMITS,
@@ -164,6 +172,7 @@ def test_image_convert_inspection_reports_error(self):
164172
convert.execute, 'file:///test/path.raw')
165173
exc_mock.assert_called_once_with(
166174
'qemu-img', 'info',
175+
'-f', 'qcow2',
167176
'--output=json',
168177
'/test/path.raw',
169178
prlimit=async_utils.QEMU_IMG_PROC_LIMITS,
@@ -200,14 +209,44 @@ def test_image_convert_invalid_qcow_data_file(self):
200209
self.assertEqual('QCOW images with data-file set are not allowed',
201210
str(e))
202211

212+
def test_image_convert_no_inspector_match(self):
213+
convert = self._setup_image_convert_info_fail()
214+
self.inspector_mock.return_value = None
215+
self.assertRaisesRegex(RuntimeError,
216+
'Unable to convert from format',
217+
convert.execute, 'file:///test/path.hpfs')
218+
219+
def test_image_convert_fails_inspection_safety_check(self):
220+
convert = self._setup_image_convert_info_fail()
221+
inspector = self.inspector_mock.return_value.from_file.return_value
222+
inspector.safety_check.return_value = False
223+
self.assertRaisesRegex(RuntimeError,
224+
'Image has disallowed configuration',
225+
convert.execute, 'file:///test/path.qcow')
226+
227+
def test_image_convert_fails_inspection_format_check(self):
228+
convert = self._setup_image_convert_info_fail()
229+
self.inspector_mock.return_value.from_file.side_effect = (
230+
format_inspector.ImageFormatError())
231+
self.assertRaisesRegex(RuntimeError,
232+
'Image format detection failed',
233+
convert.execute, 'file:///test/path.qcow')
234+
235+
def test_image_convert_fails_inspection_error(self):
236+
convert = self._setup_image_convert_info_fail()
237+
self.inspector_mock.return_value.from_file.side_effect = ValueError
238+
self.assertRaisesRegex(RuntimeError,
239+
'Unable to inspect image',
240+
convert.execute, 'file:///test/path.qcow')
241+
203242
def _test_image_convert_invalid_vmdk(self):
204243
data = {'format': 'vmdk',
205244
'format-specific': {
206245
'data': {
207246
'create-type': 'monolithicFlat',
208247
}}}
209248

210-
convert = self._setup_image_convert_info_fail()
249+
convert = self._setup_image_convert_info_fail(disk_format='vmdk')
211250
with mock.patch.object(processutils, 'execute') as exc_mock:
212251
exc_mock.return_value = json.dumps(data), ''
213252
convert.execute('file:///test/path.vmdk')
@@ -236,14 +275,15 @@ def test_image_convert_valid_vmdk(self):
236275
self._test_image_convert_invalid_vmdk)
237276

238277
def test_image_convert_fails(self):
239-
convert = self._setup_image_convert_info_fail()
278+
convert = self._setup_image_convert_info_fail(disk_format='raw')
240279
with mock.patch.object(processutils, 'execute') as exc_mock:
241280
exc_mock.side_effect = [('{"format":"raw"}', ''),
242281
OSError('convert_fail')]
243282
self.assertRaises(OSError,
244283
convert.execute, 'file:///test/path.raw')
245284
exc_mock.assert_has_calls(
246285
[mock.call('qemu-img', 'info',
286+
'-f', 'raw',
247287
'--output=json',
248288
'/test/path.raw',
249289
prlimit=async_utils.QEMU_IMG_PROC_LIMITS,
@@ -256,14 +296,15 @@ def test_image_convert_fails(self):
256296
self.img_repo.save.assert_not_called()
257297

258298
def test_image_convert_reports_fail(self):
259-
convert = self._setup_image_convert_info_fail()
299+
convert = self._setup_image_convert_info_fail(disk_format='raw')
260300
with mock.patch.object(processutils, 'execute') as exc_mock:
261301
exc_mock.side_effect = [('{"format":"raw"}', ''),
262302
('', 'some error')]
263303
self.assertRaises(RuntimeError,
264304
convert.execute, 'file:///test/path.raw')
265305
exc_mock.assert_has_calls(
266306
[mock.call('qemu-img', 'info',
307+
'-f', 'raw',
267308
'--output=json',
268309
'/test/path.raw',
269310
prlimit=async_utils.QEMU_IMG_PROC_LIMITS,
@@ -281,9 +322,10 @@ def test_image_convert_fails_source_format(self):
281322
exc_mock.return_value = ('{}', '')
282323
exc = self.assertRaises(RuntimeError,
283324
convert.execute, 'file:///test/path.raw')
284-
self.assertIn('Source format not reported', str(exc))
325+
self.assertIn('Image metadata disagrees about format', str(exc))
285326
exc_mock.assert_called_once_with(
286327
'qemu-img', 'info',
328+
'-f', 'qcow2',
287329
'--output=json',
288330
'/test/path.raw',
289331
prlimit=async_utils.QEMU_IMG_PROC_LIMITS,
@@ -301,6 +343,7 @@ def test_image_convert_same_format_does_nothing(self):
301343
# Make sure we only called qemu-img for inspection, not conversion
302344
exc_mock.assert_called_once_with(
303345
'qemu-img', 'info',
346+
'-f', 'qcow2',
304347
'--output=json',
305348
'/test/path.qcow',
306349
prlimit=async_utils.QEMU_IMG_PROC_LIMITS,

0 commit comments

Comments
 (0)