Skip to content

Commit 30fa701

Browse files
Zuulopenstack-gerrit
authored andcommitted
Merge "Add VMDK safety check" into stable/2023.1
2 parents 818654f + 812e56d commit 30fa701

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)