Skip to content

Commit 812e56d

Browse files
kk7dskonan-abhi
authored andcommitted
Add VMDK safety check
This makes us check the extent filenames to make sure they don't have any banned characters in them (i.e. slashes). It also makes us reject VMDK files with a footer. Since we process these files as a stream, we can't honor a footer that directs us to find the descriptor block in a location we've already processed. Thus, if a file indicates it has a footer, consider it a policy exception and unsupported. Change-Id: I4a1c6dff7854c49940a0ac7988722aa6befc04fa (cherry picked from commit c1bf35dffb7f4c3090b1f04cf0e27cb431330c3e) (cherry picked from commit d3f1d6159c0218ac01e8d881e2ec4da71fc952ee) (cherry picked from commit 2dd4d138d4b8e1d9ca69fc0dda3711553a65d912)
1 parent 4860024 commit 812e56d

File tree

2 files changed

+118
-2
lines changed

2 files changed

+118
-2
lines changed

glance/common/format_inspector.py

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -650,6 +650,7 @@ class VMDKInspector(FileInspector):
650650
# at 0x200 and 1MB - 1
651651
DESC_OFFSET = 0x200
652652
DESC_MAX_SIZE = (1 << 20) - 1
653+
GD_AT_END = 0xffffffffffffffff
653654

654655
def __init__(self, *a, **k):
655656
super(VMDKInspector, self).__init__(*a, **k)
@@ -662,15 +663,21 @@ def post_process(self):
662663
if not self.region('header').complete:
663664
return
664665

665-
sig, ver, _flags, _sectors, _grain, desc_sec, desc_num = struct.unpack(
666-
'<4sIIQQQQ', self.region('header').data[:44])
666+
(sig, ver, _flags, _sectors, _grain, desc_sec, desc_num,
667+
_numGTEsperGT, _rgdOffset, gdOffset) = struct.unpack(
668+
'<4sIIQQQQIQQ', self.region('header').data[:64])
667669

668670
if sig != b'KDMV':
669671
raise ImageFormatError('Signature KDMV not found: %r' % sig)
670672

671673
if ver not in (1, 2, 3):
672674
raise ImageFormatError('Unsupported format version %i' % ver)
673675

676+
if gdOffset == self.GD_AT_END:
677+
# This means we have a footer, which takes precedence over the
678+
# header, which we cannot support since we stream.
679+
raise ImageFormatError('Unsupported VMDK footer')
680+
674681
# Since we parse both desc_sec and desc_num (the location of the
675682
# VMDK's descriptor, expressed in 512 bytes sectors) we enforce a
676683
# check on the bounds to create a reasonable CaptureRegion. This
@@ -718,6 +725,59 @@ def virtual_size(self):
718725

719726
return sectors * 512
720727

728+
def safety_check(self):
729+
if (not self.has_region('descriptor') or
730+
not self.region('descriptor').complete):
731+
return False
732+
733+
try:
734+
# Descriptor is padded to 512 bytes
735+
desc_data = self.region('descriptor').data.rstrip(b'\x00')
736+
# Descriptor is actually case-insensitive ASCII text
737+
desc_text = desc_data.decode('ascii').lower()
738+
except UnicodeDecodeError:
739+
LOG.error('VMDK descriptor failed to decode as ASCII')
740+
raise ImageFormatError('Invalid VMDK descriptor data')
741+
742+
extent_access = ('rw', 'rdonly', 'noaccess')
743+
header_fields = []
744+
extents = []
745+
ddb = []
746+
747+
# NOTE(danms): Cautiously parse the VMDK descriptor. Each line must
748+
# be something we understand, otherwise we refuse it.
749+
for line in [x.strip() for x in desc_text.split('\n')]:
750+
if line.startswith('#') or not line:
751+
# Blank or comment lines are ignored
752+
continue
753+
elif line.startswith('ddb'):
754+
# DDB lines are allowed (but not used by us)
755+
ddb.append(line)
756+
elif '=' in line and ' ' not in line.split('=')[0]:
757+
# Header fields are a single word followed by an '=' and some
758+
# value
759+
header_fields.append(line)
760+
elif line.split(' ')[0] in extent_access:
761+
# Extent lines start with one of the three access modes
762+
extents.append(line)
763+
else:
764+
# Anything else results in a rejection
765+
LOG.error('Unsupported line %r in VMDK descriptor', line)
766+
raise ImageFormatError('Invalid VMDK descriptor data')
767+
768+
# Check all the extent lines for concerning content
769+
for extent_line in extents:
770+
if '/' in extent_line:
771+
LOG.error('Extent line %r contains unsafe characters',
772+
extent_line)
773+
return False
774+
775+
if not extents:
776+
LOG.error('VMDK file specified no extents')
777+
return False
778+
779+
return True
780+
721781
def __str__(self):
722782
return 'vmdk'
723783

glance/tests/unit/common/test_format_inspector.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,62 @@ def test_qcow2_feature_flag_checks(self):
307307
data[0x4F] = 0x80
308308
self.assertTrue(inspector.has_unknown_features)
309309

310+
def test_vmdk_safety_checks(self):
311+
region = format_inspector.CaptureRegion(0, 0)
312+
inspector = format_inspector.VMDKInspector()
313+
inspector.new_region('descriptor', region)
314+
315+
# This should be a legit VMDK descriptor which comments, blank lines,
316+
# an extent, some ddb content, and some header values.
317+
legit_desc = ['# This is a comment',
318+
'',
319+
' ',
320+
'createType=monolithicSparse',
321+
'RW 1234 SPARSE "foo.vmdk"',
322+
'ddb.adapterType = "MFM',
323+
'# EOF']
324+
region.data = ('\n'.join(legit_desc)).encode('ascii')
325+
region.length = len(region.data)
326+
self.assertTrue(inspector.safety_check())
327+
328+
# Any of these lines should trigger an error indicating that there is
329+
# something in the descriptor we don't understand
330+
bad_lines = [
331+
'#\U0001F4A9',
332+
'header Name=foo',
333+
'foo bar',
334+
'WR 123 SPARSE "foo.vmdk"',
335+
]
336+
337+
for bad_line in bad_lines:
338+
# Encode as UTF-8 purely so we can test that anything non-ASCII
339+
# will trigger the decode check
340+
region.data = bad_line.encode('utf-8')
341+
region.length = len(region.data)
342+
self.assertRaisesRegex(format_inspector.ImageFormatError,
343+
'Invalid VMDK descriptor',
344+
inspector.safety_check)
345+
346+
# Extents with slashes in the name fail the safety check
347+
region.data = b'RW 123 SPARSE "/etc/shadow"'
348+
region.length = len(region.data)
349+
self.assertFalse(inspector.safety_check())
350+
351+
# A descriptor that specifies no extents fails the safety check
352+
region.data = b'# Nothing'
353+
region.length = len(region.data)
354+
self.assertFalse(inspector.safety_check())
355+
356+
def test_vmdk_reject_footer(self):
357+
data = struct.pack('<4sIIQQQQIQQ', b'KDMV', 3, 0, 0, 0, 0, 1, 0, 0,
358+
format_inspector.VMDKInspector.GD_AT_END)
359+
inspector = format_inspector.VMDKInspector()
360+
inspector.region('header').data = data
361+
inspector.region('header').length = len(data)
362+
self.assertRaisesRegex(format_inspector.ImageFormatError,
363+
'footer',
364+
inspector.post_process)
365+
310366
def test_vdi(self):
311367
self._test_format('vdi')
312368

0 commit comments

Comments
 (0)