Skip to content

Commit 06a1820

Browse files
quatrekonan-abhi
authored andcommitted
Limit CaptureRegion sizes in format_inspector for VMDK and VHDX
VMDK: When parsing a VMDK file to calculate its size, the format_inspector determines the location of the Descriptor section by reading two uint64 from the headers of the file and uses them to create the descriptor CaptureRegion. It would be possible to craft a VMDK file that commands the format_inspector to create a very big CaptureRegion, thus exhausting resources on the glance-api process. This patch binds the beginning of the descriptor to 0x200 and limits the size of the CaptureRegion to 1MB, similar to how the VMDK descriptor is parsed by qemu. VHDX: It is a bit more involved, but similar: when looking for the VIRTUAL_DISK_SIZE metadata, the format_inspector was creating an unbounded CaptureRegion. In the same way as it seems to be done in Qemu, we now limit the upper bound of this CaptureRegion. Closes-Bug: #2006490 Change-Id: I3ec5a33df20e1cfb6673f4ff1c7c91aacd065532 (cherry picked from commit d4d33ee)
1 parent b1e5292 commit 06a1820

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)