Codebase list glance / 3929db2
Merge "Limit CaptureRegion sizes in format_inspector for VMDK and VHDX" Zuul authored 1 year, 3 months ago Gerrit Code Review committed 1 year, 3 months ago
2 changed file(s) with 139 addition(s) and 3 deletion(s). Raw diff Collapse all Expand all
344344 """
345345 METAREGION = '8B7CA206-4790-4B9A-B8FE-575F050F886E'
346346 VIRTUAL_DISK_SIZE = '2FA54224-CD1B-4876-B211-5DBED83BF4B8'
347 VHDX_METADATA_TABLE_MAX_SIZE = 32 * 2048 # From qemu
347348
348349 def __init__(self, *a, **k):
349350 super(VHDXInspector, self).__init__(*a, **k)
458459 item_offset, item_length, _reserved = struct.unpack(
459460 '<III',
460461 meta_buffer[entry_offset + 16:entry_offset + 28])
462 item_length = min(item_length,
463 self.VHDX_METADATA_TABLE_MAX_SIZE)
461464 self.region('metadata').length = len(meta_buffer)
462465 self._log.debug('Found entry at offset %x', item_offset)
463466 # Metadata item offset is from the beginning of the metadata
515518 variable number of 512 byte sectors, but is just text defining the
516519 layout of the disk.
517520 """
521
522 # The beginning and max size of the descriptor is also hardcoded in Qemu
523 # at 0x200 and 1MB - 1
524 DESC_OFFSET = 0x200
525 DESC_MAX_SIZE = (1 << 20) - 1
526
518527 def __init__(self, *a, **k):
519528 super(VMDKInspector, self).__init__(*a, **k)
520529 self.new_region('header', CaptureRegion(0, 512))
531540
532541 if sig != b'KDMV':
533542 raise ImageFormatError('Signature KDMV not found: %r' % sig)
534 return
535543
536544 if ver not in (1, 2, 3):
537545 raise ImageFormatError('Unsupported format version %i' % ver)
538 return
546
547 # Since we parse both desc_sec and desc_num (the location of the
548 # VMDK's descriptor, expressed in 512 bytes sectors) we enforce a
549 # check on the bounds to create a reasonable CaptureRegion. This
550 # is similar to how it's done in qemu.
551 desc_offset = desc_sec * 512
552 desc_size = min(desc_num * 512, self.DESC_MAX_SIZE)
553 if desc_offset != self.DESC_OFFSET:
554 raise ImageFormatError("Wrong descriptor location")
539555
540556 if not self.has_region('descriptor'):
541557 self.new_region('descriptor', CaptureRegion(
542 desc_sec * 512, desc_num * 512))
558 desc_offset, desc_size))
543559
544560 @property
545561 def format_match(self):
1515 import io
1616 import os
1717 import re
18 import struct
1819 import subprocess
1920 import tempfile
2021 from unittest import mock
5960 self._created_files.append(fn)
6061 subprocess.check_output(
6162 'qemu-img create -f %s %s %i' % (fmt, fn, size),
63 shell=True)
64 return fn
65
66 def _create_allocated_vmdk(self, size_mb):
67 # We need a "big" VMDK file to exercise some parts of the code of the
68 # format_inspector. A way to create one is to first create an empty
69 # file, and then to convert it with the -S 0 option.
70 fn = tempfile.mktemp(prefix='glance-unittest-formatinspector-',
71 suffix='.vmdk')
72 self._created_files.append(fn)
73 zeroes = tempfile.mktemp(prefix='glance-unittest-formatinspector-',
74 suffix='.zero')
75 self._created_files.append(zeroes)
76
77 # Create an empty file
78 subprocess.check_output(
79 'dd if=/dev/zero of=%s bs=1M count=%i' % (zeroes, size_mb),
80 shell=True)
81
82 # Convert it to VMDK
83 subprocess.check_output(
84 'qemu-img convert -f raw -O vmdk -S 0 %s %s' % (zeroes, fn),
6285 shell=True)
6386 return fn
6487
118141 def test_vmdk(self):
119142 self._test_format('vmdk')
120143
144 def test_vmdk_bad_descriptor_offset(self):
145 format_name = 'vmdk'
146 image_size = 10 * units.Mi
147 descriptorOffsetAddr = 0x1c
148 BAD_ADDRESS = 0x400
149 img = self._create_img(format_name, image_size)
150
151 # Corrupt the header
152 fd = open(img, 'r+b')
153 fd.seek(descriptorOffsetAddr)
154 fd.write(struct.pack('<Q', BAD_ADDRESS // 512))
155 fd.close()
156
157 # Read the format in various sizes, some of which will read whole
158 # sections in a single read, others will be completely unaligned, etc.
159 for block_size in (64 * units.Ki, 512, 17, 1 * units.Mi):
160 fmt = self._test_format_at_block_size(format_name, img, block_size)
161 self.assertTrue(fmt.format_match,
162 'Failed to match %s at size %i block %i' % (
163 format_name, image_size, block_size))
164 self.assertEqual(0, fmt.virtual_size,
165 ('Calculated a virtual size for a corrupt %s at '
166 'size %i block %i') % (format_name, image_size,
167 block_size))
168
169 def test_vmdk_bad_descriptor_mem_limit(self):
170 format_name = 'vmdk'
171 image_size = 5 * units.Mi
172 virtual_size = 5 * units.Mi
173 descriptorOffsetAddr = 0x1c
174 descriptorSizeAddr = descriptorOffsetAddr + 8
175 twoMBInSectors = (2 << 20) // 512
176 # We need a big VMDK because otherwise we will not have enough data to
177 # fill-up the CaptureRegion.
178 img = self._create_allocated_vmdk(image_size // units.Mi)
179
180 # Corrupt the end of descriptor address so it "ends" at 2MB
181 fd = open(img, 'r+b')
182 fd.seek(descriptorSizeAddr)
183 fd.write(struct.pack('<Q', twoMBInSectors))
184 fd.close()
185
186 # Read the format in various sizes, some of which will read whole
187 # sections in a single read, others will be completely unaligned, etc.
188 for block_size in (64 * units.Ki, 512, 17, 1 * units.Mi):
189 fmt = self._test_format_at_block_size(format_name, img, block_size)
190 self.assertTrue(fmt.format_match,
191 'Failed to match %s at size %i block %i' % (
192 format_name, image_size, block_size))
193 self.assertEqual(virtual_size, fmt.virtual_size,
194 ('Failed to calculate size for %s at size %i '
195 'block %i') % (format_name, image_size,
196 block_size))
197 memory = sum(fmt.context_info.values())
198 self.assertLess(memory, 1.5 * units.Mi,
199 'Format used more than 1.5MiB of memory: %s' % (
200 fmt.context_info))
201
121202 def test_vdi(self):
122203 self._test_format('vdi')
123204
274355 self.assertEqual(format_inspector.QcowInspector,
275356 format_inspector.get_inspector('qcow2'))
276357 self.assertIsNone(format_inspector.get_inspector('foo'))
358
359
360 class TestFormatInspectorsTargeted(test_utils.BaseTestCase):
361 def _make_vhd_meta(self, guid_raw, item_length):
362 # Meta region header, padded to 32 bytes
363 data = struct.pack('<8sHH', b'metadata', 0, 1)
364 data += b'0' * 20
365
366 # Metadata table entry, 16-byte GUID, 12-byte information,
367 # padded to 32-bytes
368 data += guid_raw
369 data += struct.pack('<III', 256, item_length, 0)
370 data += b'0' * 6
371
372 return data
373
374 def test_vhd_table_over_limit(self):
375 ins = format_inspector.VHDXInspector()
376 meta = format_inspector.CaptureRegion(0, 0)
377 desired = b'012345678ABCDEF0'
378 # This is a poorly-crafted image that specifies a larger table size
379 # than is allowed
380 meta.data = self._make_vhd_meta(desired, 33 * 2048)
381 ins.new_region('metadata', meta)
382 new_region = ins._find_meta_entry(ins._guid(desired))
383 # Make sure we clamp to our limit of 32 * 2048
384 self.assertEqual(
385 format_inspector.VHDXInspector.VHDX_METADATA_TABLE_MAX_SIZE,
386 new_region.length)
387
388 def test_vhd_table_under_limit(self):
389 ins = format_inspector.VHDXInspector()
390 meta = format_inspector.CaptureRegion(0, 0)
391 desired = b'012345678ABCDEF0'
392 meta.data = self._make_vhd_meta(desired, 16 * 2048)
393 ins.new_region('metadata', meta)
394 new_region = ins._find_meta_entry(ins._guid(desired))
395 # Table size was under the limit, make sure we get it back
396 self.assertEqual(16 * 2048, new_region.length)