Skip to content

Commit 3929db2

Browse files
Zuulopenstack-gerrit
authored andcommitted
Merge "Limit CaptureRegion sizes in format_inspector for VMDK and VHDX"
2 parents e39ef4e + d4d33ee commit 3929db2

File tree

2 files changed

+139
-3
lines changed

2 files changed

+139
-3
lines changed

glance/common/format_inspector.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,7 @@ class VHDXInspector(FileInspector):
345345
"""
346346
METAREGION = '8B7CA206-4790-4B9A-B8FE-575F050F886E'
347347
VIRTUAL_DISK_SIZE = '2FA54224-CD1B-4876-B211-5DBED83BF4B8'
348+
VHDX_METADATA_TABLE_MAX_SIZE = 32 * 2048 # From qemu
348349

349350
def __init__(self, *a, **k):
350351
super(VHDXInspector, self).__init__(*a, **k)
@@ -459,6 +460,8 @@ def _find_meta_entry(self, desired_guid):
459460
item_offset, item_length, _reserved = struct.unpack(
460461
'<III',
461462
meta_buffer[entry_offset + 16:entry_offset + 28])
463+
item_length = min(item_length,
464+
self.VHDX_METADATA_TABLE_MAX_SIZE)
462465
self.region('metadata').length = len(meta_buffer)
463466
self._log.debug('Found entry at offset %x', item_offset)
464467
# Metadata item offset is from the beginning of the metadata
@@ -516,6 +519,12 @@ class VMDKInspector(FileInspector):
516519
variable number of 512 byte sectors, but is just text defining the
517520
layout of the disk.
518521
"""
522+
523+
# The beginning and max size of the descriptor is also hardcoded in Qemu
524+
# at 0x200 and 1MB - 1
525+
DESC_OFFSET = 0x200
526+
DESC_MAX_SIZE = (1 << 20) - 1
527+
519528
def __init__(self, *a, **k):
520529
super(VMDKInspector, self).__init__(*a, **k)
521530
self.new_region('header', CaptureRegion(0, 512))
@@ -532,15 +541,22 @@ def post_process(self):
532541

533542
if sig != b'KDMV':
534543
raise ImageFormatError('Signature KDMV not found: %r' % sig)
535-
return
536544

537545
if ver not in (1, 2, 3):
538546
raise ImageFormatError('Unsupported format version %i' % ver)
539-
return
547+
548+
# Since we parse both desc_sec and desc_num (the location of the
549+
# VMDK's descriptor, expressed in 512 bytes sectors) we enforce a
550+
# check on the bounds to create a reasonable CaptureRegion. This
551+
# is similar to how it's done in qemu.
552+
desc_offset = desc_sec * 512
553+
desc_size = min(desc_num * 512, self.DESC_MAX_SIZE)
554+
if desc_offset != self.DESC_OFFSET:
555+
raise ImageFormatError("Wrong descriptor location")
540556

541557
if not self.has_region('descriptor'):
542558
self.new_region('descriptor', CaptureRegion(
543-
desc_sec * 512, desc_num * 512))
559+
desc_offset, desc_size))
544560

545561
@property
546562
def format_match(self):

glance/tests/unit/common/test_format_inspector.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import io
1717
import os
1818
import re
19+
import struct
1920
import subprocess
2021
import tempfile
2122
from unittest import mock
@@ -63,6 +64,28 @@ def _create_img(self, fmt, size):
6364
shell=True)
6465
return fn
6566

67+
def _create_allocated_vmdk(self, size_mb):
68+
# We need a "big" VMDK file to exercise some parts of the code of the
69+
# format_inspector. A way to create one is to first create an empty
70+
# file, and then to convert it with the -S 0 option.
71+
fn = tempfile.mktemp(prefix='glance-unittest-formatinspector-',
72+
suffix='.vmdk')
73+
self._created_files.append(fn)
74+
zeroes = tempfile.mktemp(prefix='glance-unittest-formatinspector-',
75+
suffix='.zero')
76+
self._created_files.append(zeroes)
77+
78+
# Create an empty file
79+
subprocess.check_output(
80+
'dd if=/dev/zero of=%s bs=1M count=%i' % (zeroes, size_mb),
81+
shell=True)
82+
83+
# Convert it to VMDK
84+
subprocess.check_output(
85+
'qemu-img convert -f raw -O vmdk -S 0 %s %s' % (zeroes, fn),
86+
shell=True)
87+
return fn
88+
6689
def _test_format_at_block_size(self, format_name, img, block_size):
6790
fmt = format_inspector.get_inspector(format_name)()
6891
self.assertIsNotNone(fmt,
@@ -119,6 +142,64 @@ def test_vhdx(self):
119142
def test_vmdk(self):
120143
self._test_format('vmdk')
121144

145+
def test_vmdk_bad_descriptor_offset(self):
146+
format_name = 'vmdk'
147+
image_size = 10 * units.Mi
148+
descriptorOffsetAddr = 0x1c
149+
BAD_ADDRESS = 0x400
150+
img = self._create_img(format_name, image_size)
151+
152+
# Corrupt the header
153+
fd = open(img, 'r+b')
154+
fd.seek(descriptorOffsetAddr)
155+
fd.write(struct.pack('<Q', BAD_ADDRESS // 512))
156+
fd.close()
157+
158+
# Read the format in various sizes, some of which will read whole
159+
# sections in a single read, others will be completely unaligned, etc.
160+
for block_size in (64 * units.Ki, 512, 17, 1 * units.Mi):
161+
fmt = self._test_format_at_block_size(format_name, img, block_size)
162+
self.assertTrue(fmt.format_match,
163+
'Failed to match %s at size %i block %i' % (
164+
format_name, image_size, block_size))
165+
self.assertEqual(0, fmt.virtual_size,
166+
('Calculated a virtual size for a corrupt %s at '
167+
'size %i block %i') % (format_name, image_size,
168+
block_size))
169+
170+
def test_vmdk_bad_descriptor_mem_limit(self):
171+
format_name = 'vmdk'
172+
image_size = 5 * units.Mi
173+
virtual_size = 5 * units.Mi
174+
descriptorOffsetAddr = 0x1c
175+
descriptorSizeAddr = descriptorOffsetAddr + 8
176+
twoMBInSectors = (2 << 20) // 512
177+
# We need a big VMDK because otherwise we will not have enough data to
178+
# fill-up the CaptureRegion.
179+
img = self._create_allocated_vmdk(image_size // units.Mi)
180+
181+
# Corrupt the end of descriptor address so it "ends" at 2MB
182+
fd = open(img, 'r+b')
183+
fd.seek(descriptorSizeAddr)
184+
fd.write(struct.pack('<Q', twoMBInSectors))
185+
fd.close()
186+
187+
# Read the format in various sizes, some of which will read whole
188+
# sections in a single read, others will be completely unaligned, etc.
189+
for block_size in (64 * units.Ki, 512, 17, 1 * units.Mi):
190+
fmt = self._test_format_at_block_size(format_name, img, block_size)
191+
self.assertTrue(fmt.format_match,
192+
'Failed to match %s at size %i block %i' % (
193+
format_name, image_size, block_size))
194+
self.assertEqual(virtual_size, fmt.virtual_size,
195+
('Failed to calculate size for %s at size %i '
196+
'block %i') % (format_name, image_size,
197+
block_size))
198+
memory = sum(fmt.context_info.values())
199+
self.assertLess(memory, 1.5 * units.Mi,
200+
'Format used more than 1.5MiB of memory: %s' % (
201+
fmt.context_info))
202+
122203
def test_vdi(self):
123204
self._test_format('vdi')
124205

@@ -275,3 +356,42 @@ def test_get_inspector(self):
275356
self.assertEqual(format_inspector.QcowInspector,
276357
format_inspector.get_inspector('qcow2'))
277358
self.assertIsNone(format_inspector.get_inspector('foo'))
359+
360+
361+
class TestFormatInspectorsTargeted(test_utils.BaseTestCase):
362+
def _make_vhd_meta(self, guid_raw, item_length):
363+
# Meta region header, padded to 32 bytes
364+
data = struct.pack('<8sHH', b'metadata', 0, 1)
365+
data += b'0' * 20
366+
367+
# Metadata table entry, 16-byte GUID, 12-byte information,
368+
# padded to 32-bytes
369+
data += guid_raw
370+
data += struct.pack('<III', 256, item_length, 0)
371+
data += b'0' * 6
372+
373+
return data
374+
375+
def test_vhd_table_over_limit(self):
376+
ins = format_inspector.VHDXInspector()
377+
meta = format_inspector.CaptureRegion(0, 0)
378+
desired = b'012345678ABCDEF0'
379+
# This is a poorly-crafted image that specifies a larger table size
380+
# than is allowed
381+
meta.data = self._make_vhd_meta(desired, 33 * 2048)
382+
ins.new_region('metadata', meta)
383+
new_region = ins._find_meta_entry(ins._guid(desired))
384+
# Make sure we clamp to our limit of 32 * 2048
385+
self.assertEqual(
386+
format_inspector.VHDXInspector.VHDX_METADATA_TABLE_MAX_SIZE,
387+
new_region.length)
388+
389+
def test_vhd_table_under_limit(self):
390+
ins = format_inspector.VHDXInspector()
391+
meta = format_inspector.CaptureRegion(0, 0)
392+
desired = b'012345678ABCDEF0'
393+
meta.data = self._make_vhd_meta(desired, 16 * 2048)
394+
ins.new_region('metadata', meta)
395+
new_region = ins._find_meta_entry(ins._guid(desired))
396+
# Table size was under the limit, make sure we get it back
397+
self.assertEqual(16 * 2048, new_region.length)

0 commit comments

Comments
 (0)