Skip to content

Commit 0707419

Browse files
Zuulopenstack-gerrit
authored andcommitted
Merge "Reject unsafe qcow and vmdk files" into unmaintained/yoga
2 parents 65fd03a + 2923885 commit 0707419

File tree

3 files changed

+91
-14
lines changed

3 files changed

+91
-14
lines changed

glance/async_/flows/api_image_import.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,10 @@ def image_locations(self):
207207
# should have moderated access like all the other things here.
208208
return copy.deepcopy(self._image.locations)
209209

210+
@property
211+
def image_disk_format(self):
212+
return self._image.disk_format
213+
210214
@property
211215
def image_status(self):
212216
return self._image.status

glance/async_/flows/plugins/image_conversion.py

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

2828
from glance.async_ import utils
29+
from glance.common import format_inspector
2930
from glance.i18n import _, _LI
3031

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

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

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

117147
virtual_size = metadata.get('virtual-size', 0)
118148
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
import sys
@@ -25,6 +26,7 @@
2526
import glance.async_.flows.api_image_import as import_flow
2627
import glance.async_.flows.plugins.image_conversion as image_conversion
2728
from glance.async_ import utils as async_utils
29+
from glance.common import format_inspector
2830
from glance.common import utils
2931
from glance import domain
3032
from glance import gateway
@@ -91,6 +93,11 @@ def setUp(self):
9193
self.image_id,
9294
self.task.task_id)
9395

96+
self.inspector_mock = mock.MagicMock()
97+
self.useFixture(fixtures.MockPatch('glance.common.format_inspector.'
98+
'get_inspector',
99+
self.inspector_mock))
100+
94101
@mock.patch.object(os, 'stat')
95102
@mock.patch.object(os, 'remove')
96103
def test_image_convert_success(self, mock_os_remove, mock_os_stat):
@@ -105,7 +112,7 @@ def test_image_convert_success(self, mock_os_remove, mock_os_stat):
105112
image = mock.MagicMock(image_id=self.image_id, virtual_size=None,
106113
extra_properties={
107114
'os_glance_import_task': self.task.task_id},
108-
disk_format='qcow2')
115+
disk_format='raw')
109116
self.img_repo.get.return_value = image
110117

111118
with mock.patch.object(processutils, 'execute') as exc_mock:
@@ -127,7 +134,7 @@ def test_image_convert_success(self, mock_os_remove, mock_os_stat):
127134
self.assertEqual(456, image.virtual_size)
128135
self.assertEqual(123, image.size)
129136

130-
def _setup_image_convert_info_fail(self):
137+
def _setup_image_convert_info_fail(self, disk_format='qcow2'):
131138
image_convert = image_conversion._ConvertImage(self.context,
132139
self.task.task_id,
133140
self.task_type,
@@ -137,7 +144,7 @@ def _setup_image_convert_info_fail(self):
137144
image = mock.MagicMock(image_id=self.image_id, virtual_size=None,
138145
extra_properties={
139146
'os_glance_import_task': self.task.task_id},
140-
disk_format='qcow2')
147+
disk_format=disk_format)
141148
self.img_repo.get.return_value = image
142149
return image_convert
143150

@@ -149,6 +156,7 @@ def test_image_convert_fails_inspection(self):
149156
convert.execute, 'file:///test/path.raw')
150157
exc_mock.assert_called_once_with(
151158
'qemu-img', 'info',
159+
'-f', 'qcow2',
152160
'--output=json',
153161
'/test/path.raw',
154162
prlimit=async_utils.QEMU_IMG_PROC_LIMITS,
@@ -165,6 +173,7 @@ def test_image_convert_inspection_reports_error(self):
165173
convert.execute, 'file:///test/path.raw')
166174
exc_mock.assert_called_once_with(
167175
'qemu-img', 'info',
176+
'-f', 'qcow2',
168177
'--output=json',
169178
'/test/path.raw',
170179
prlimit=async_utils.QEMU_IMG_PROC_LIMITS,
@@ -201,14 +210,44 @@ def test_image_convert_invalid_qcow_data_file(self):
201210
self.assertEqual('QCOW images with data-file set are not allowed',
202211
str(e))
203212

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

211-
convert = self._setup_image_convert_info_fail()
250+
convert = self._setup_image_convert_info_fail(disk_format='vmdk')
212251
with mock.patch.object(processutils, 'execute') as exc_mock:
213252
exc_mock.return_value = json.dumps(data), ''
214253
convert.execute('file:///test/path.vmdk')
@@ -237,14 +276,15 @@ def test_image_convert_valid_vmdk(self):
237276
self._test_image_convert_invalid_vmdk)
238277

239278
def test_image_convert_fails(self):
240-
convert = self._setup_image_convert_info_fail()
279+
convert = self._setup_image_convert_info_fail(disk_format='raw')
241280
with mock.patch.object(processutils, 'execute') as exc_mock:
242281
exc_mock.side_effect = [('{"format":"raw"}', ''),
243282
OSError('convert_fail')]
244283
self.assertRaises(OSError,
245284
convert.execute, 'file:///test/path.raw')
246285
exc_mock.assert_has_calls(
247286
[mock.call('qemu-img', 'info',
287+
'-f', 'raw',
248288
'--output=json',
249289
'/test/path.raw',
250290
prlimit=async_utils.QEMU_IMG_PROC_LIMITS,
@@ -257,14 +297,15 @@ def test_image_convert_fails(self):
257297
self.img_repo.save.assert_not_called()
258298

259299
def test_image_convert_reports_fail(self):
260-
convert = self._setup_image_convert_info_fail()
300+
convert = self._setup_image_convert_info_fail(disk_format='raw')
261301
with mock.patch.object(processutils, 'execute') as exc_mock:
262302
exc_mock.side_effect = [('{"format":"raw"}', ''),
263303
('', 'some error')]
264304
self.assertRaises(RuntimeError,
265305
convert.execute, 'file:///test/path.raw')
266306
exc_mock.assert_has_calls(
267307
[mock.call('qemu-img', 'info',
308+
'-f', 'raw',
268309
'--output=json',
269310
'/test/path.raw',
270311
prlimit=async_utils.QEMU_IMG_PROC_LIMITS,
@@ -282,9 +323,10 @@ def test_image_convert_fails_source_format(self):
282323
exc_mock.return_value = ('{}', '')
283324
exc = self.assertRaises(RuntimeError,
284325
convert.execute, 'file:///test/path.raw')
285-
self.assertIn('Source format not reported', str(exc))
326+
self.assertIn('Image metadata disagrees about format', str(exc))
286327
exc_mock.assert_called_once_with(
287328
'qemu-img', 'info',
329+
'-f', 'qcow2',
288330
'--output=json',
289331
'/test/path.raw',
290332
prlimit=async_utils.QEMU_IMG_PROC_LIMITS,
@@ -302,6 +344,7 @@ def test_image_convert_same_format_does_nothing(self):
302344
# Make sure we only called qemu-img for inspection, not conversion
303345
exc_mock.assert_called_once_with(
304346
'qemu-img', 'info',
347+
'-f', 'qcow2',
305348
'--output=json',
306349
'/test/path.qcow',
307350
prlimit=async_utils.QEMU_IMG_PROC_LIMITS,

0 commit comments

Comments
 (0)