|
16 | 16 | import io
|
17 | 17 | import os
|
18 | 18 | import re
|
| 19 | +import struct |
19 | 20 | import subprocess
|
20 | 21 | import tempfile
|
21 | 22 | from unittest import mock
|
@@ -63,6 +64,28 @@ def _create_img(self, fmt, size):
|
63 | 64 | shell=True)
|
64 | 65 | return fn
|
65 | 66 |
|
| 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 | + |
66 | 89 | def _test_format_at_block_size(self, format_name, img, block_size):
|
67 | 90 | fmt = format_inspector.get_inspector(format_name)()
|
68 | 91 | self.assertIsNotNone(fmt,
|
@@ -119,6 +142,64 @@ def test_vhdx(self):
|
119 | 142 | def test_vmdk(self):
|
120 | 143 | self._test_format('vmdk')
|
121 | 144 |
|
| 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 | + |
122 | 203 | def test_vdi(self):
|
123 | 204 | self._test_format('vdi')
|
124 | 205 |
|
@@ -275,3 +356,42 @@ def test_get_inspector(self):
|
275 | 356 | self.assertEqual(format_inspector.QcowInspector,
|
276 | 357 | format_inspector.get_inspector('qcow2'))
|
277 | 358 | 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