Skip to content

Commit cd19a3d

Browse files
kk7dsmarkgoddard
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 c6d5a68 commit cd19a3d

File tree

2 files changed

+124
-3
lines changed

2 files changed

+124
-3
lines changed

glance/common/format_inspector.py

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -642,6 +642,13 @@ class VMDKInspector(FileInspector):
642642
variable number of 512 byte sectors, but is just text defining the
643643
layout of the disk.
644644
"""
645+
646+
# The beginning and max size of the descriptor is also hardcoded in Qemu
647+
# at 0x200 and 1MB - 1
648+
DESC_OFFSET = 0x200
649+
DESC_MAX_SIZE = (1 << 20) - 1
650+
GD_AT_END = 0xffffffffffffffff
651+
645652
def __init__(self, *a, **k):
646653
super(VMDKInspector, self).__init__(*a, **k)
647654
self.new_region('header', CaptureRegion(0, 512))
@@ -653,16 +660,21 @@ def post_process(self):
653660
if not self.region('header').complete:
654661
return
655662

656-
sig, ver, _flags, _sectors, _grain, desc_sec, desc_num = struct.unpack(
657-
'<4sIIQQQQ', self.region('header').data[:44])
663+
(sig, ver, _flags, _sectors, _grain, desc_sec, desc_num,
664+
_numGTEsperGT, _rgdOffset, gdOffset) = struct.unpack(
665+
'<4sIIQQQQIQQ', self.region('header').data[:64])
658666

659667
if sig != b'KDMV':
660668
raise ImageFormatError('Signature KDMV not found: %r' % sig)
661669
return
662670

663671
if ver not in (1, 2, 3):
664672
raise ImageFormatError('Unsupported format version %i' % ver)
665-
return
673+
674+
if gdOffset == self.GD_AT_END:
675+
# This means we have a footer, which takes precedence over the
676+
# header, which we cannot support since we stream.
677+
raise ImageFormatError('Unsupported VMDK footer')
666678

667679
if not self.has_region('descriptor'):
668680
self.new_region('descriptor', CaptureRegion(
@@ -702,6 +714,59 @@ def virtual_size(self):
702714

703715
return sectors * 512
704716

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

glance/tests/unit/common/test_format_inspector.py

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

205+
def test_vmdk_safety_checks(self):
206+
region = format_inspector.CaptureRegion(0, 0)
207+
inspector = format_inspector.VMDKInspector()
208+
inspector.new_region('descriptor', region)
209+
210+
# This should be a legit VMDK descriptor which comments, blank lines,
211+
# an extent, some ddb content, and some header values.
212+
legit_desc = ['# This is a comment',
213+
'',
214+
' ',
215+
'createType=monolithicSparse',
216+
'RW 1234 SPARSE "foo.vmdk"',
217+
'ddb.adapterType = "MFM',
218+
'# EOF']
219+
region.data = ('\n'.join(legit_desc)).encode('ascii')
220+
region.length = len(region.data)
221+
self.assertTrue(inspector.safety_check())
222+
223+
# Any of these lines should trigger an error indicating that there is
224+
# something in the descriptor we don't understand
225+
bad_lines = [
226+
'#\U0001F4A9',
227+
'header Name=foo',
228+
'foo bar',
229+
'WR 123 SPARSE "foo.vmdk"',
230+
]
231+
232+
for bad_line in bad_lines:
233+
# Encode as UTF-8 purely so we can test that anything non-ASCII
234+
# will trigger the decode check
235+
region.data = bad_line.encode('utf-8')
236+
region.length = len(region.data)
237+
self.assertRaisesRegex(format_inspector.ImageFormatError,
238+
'Invalid VMDK descriptor',
239+
inspector.safety_check)
240+
241+
# Extents with slashes in the name fail the safety check
242+
region.data = b'RW 123 SPARSE "/etc/shadow"'
243+
region.length = len(region.data)
244+
self.assertFalse(inspector.safety_check())
245+
246+
# A descriptor that specifies no extents fails the safety check
247+
region.data = b'# Nothing'
248+
region.length = len(region.data)
249+
self.assertFalse(inspector.safety_check())
250+
251+
def test_vmdk_reject_footer(self):
252+
data = struct.pack('<4sIIQQQQIQQ', b'KDMV', 3, 0, 0, 0, 0, 1, 0, 0,
253+
format_inspector.VMDKInspector.GD_AT_END)
254+
inspector = format_inspector.VMDKInspector()
255+
inspector.region('header').data = data
256+
inspector.region('header').length = len(data)
257+
self.assertRaisesRegex(format_inspector.ImageFormatError,
258+
'footer',
259+
inspector.post_process)
260+
205261
def test_vdi(self):
206262
self._test_format('vdi')
207263

0 commit comments

Comments
 (0)