diff -Nru glance-25.1.0/debian/changelog glance-25.1.0/debian/changelog --- glance-25.1.0/debian/changelog 2023-04-14 11:32:58.000000000 +0000 +++ glance-25.1.0/debian/changelog 2024-06-21 08:38:56.000000000 +0000 @@ -1,3 +1,19 @@ +glance (2:25.1.0-2+deb12u1) bookworm-security; urgency=high + + * CVE-2024-32498: Arbitrary file access through custom QCOW2 external data. + Add upstream patch (Closes: #1074761): + - CVE-2024-32498_1_Limit_CaptureRegion_sizes_in_format_inspector_for_VMDK_and_VHDX.patch + - CVE-2024-32498_2_Support_Stream_Optimized_VMDKs.patch + - CVE-2024-32498_3_1_glance-stable-2023.1.patch + - CVE-2024-32498_3_2_glance-stable-2023.1.patch + - CVE-2024-32498_3_3_glance-stable-2023.1.patch + - CVE-2024-32498_3_4_glance-stable-2023.1.patch + - CVE-2024-32498_3_5_glance-stable-2023.1.patch + - CVE-2024-32498_3_6_glance-stable-2023.1.patch + - CVE-2024-32498_3_7_glance-stable-2023.1.patch + + -- Thomas Goirand Fri, 21 Jun 2024 10:38:56 +0200 + glance (2:25.1.0-2) unstable; urgency=medium * Build-depends on openstack-pkg-tools (>= 123~). diff -Nru glance-25.1.0/debian/patches/CVE-2024-32498_1_Limit_CaptureRegion_sizes_in_format_inspector_for_VMDK_and_VHDX.patch glance-25.1.0/debian/patches/CVE-2024-32498_1_Limit_CaptureRegion_sizes_in_format_inspector_for_VMDK_and_VHDX.patch --- glance-25.1.0/debian/patches/CVE-2024-32498_1_Limit_CaptureRegion_sizes_in_format_inspector_for_VMDK_and_VHDX.patch 1970-01-01 00:00:00.000000000 +0000 +++ glance-25.1.0/debian/patches/CVE-2024-32498_1_Limit_CaptureRegion_sizes_in_format_inspector_for_VMDK_and_VHDX.patch 2024-06-21 08:38:56.000000000 +0000 @@ -0,0 +1,240 @@ +Author: Guillaume Espanel +Date: Wed, 25 Jan 2023 11:53:09 +0100 +Subject: 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. +Bug: https://launchpad.net/bugs/2006490 +Change-Id: I3ec5a33df20e1cfb6673f4ff1c7c91aacd065532 +Bug: https://launchpad.net/bugs/2059809 +Bug-Debian: https://bugs.debian.org/1074761 +Origin: upstream, https://review.opendev.org/c/openstack/glance/+/872990 +Last-Update: 2024-06-21 + +diff --git a/glance/common/format_inspector.py b/glance/common/format_inspector.py +index 351c300..550ccea 100755 +--- a/glance/common/format_inspector.py ++++ b/glance/common/format_inspector.py +@@ -345,6 +345,7 @@ + """ + METAREGION = '8B7CA206-4790-4B9A-B8FE-575F050F886E' + VIRTUAL_DISK_SIZE = '2FA54224-CD1B-4876-B211-5DBED83BF4B8' ++ VHDX_METADATA_TABLE_MAX_SIZE = 32 * 2048 # From qemu + + def __init__(self, *a, **k): + super(VHDXInspector, self).__init__(*a, **k) +@@ -459,6 +460,8 @@ + item_offset, item_length, _reserved = struct.unpack( + ' +Date: Fri, 02 Feb 2024 11:48:24 +0100 +Description: Support Stream Optimized VMDKs + Stream optimized VMDKs are also monolithic disks images, and start + with the same sparse extend header as normal monolithic sparse files, + so we can parse the virtual disk size in the same manner. + . + See "VMware Virtual Disks Virtual Disk Format 1.1" p. 17. + > Header and Footer + > The header and the footer are both described by the same SparseExtentHeader + > structure shown in Hosted Sparse Extent Header on page 8. +Bug: https://launchpad.net/bugs/2052291 +Change-Id: I7d63951ff080dc699b8d11babc0a5998d90621e4 +Co-Authored-By: Rajiv Mucheli +Bug: https://launchpad.net/bugs/2059809 +Bug-Debian: https://bugs.debian.org/1074761 +Origin: upstream, https://review.opendev.org/c/openstack/glance/+/921300 +Last-Update: 2024-06-21 + +diff --git a/glance/common/format_inspector.py b/glance/common/format_inspector.py +index 550ccea..d9576f1 100755 +--- a/glance/common/format_inspector.py ++++ b/glance/common/format_inspector.py +@@ -512,7 +512,7 @@ + # + # https://www.vmware.com/app/vmdk/?src=vmdk + class VMDKInspector(FileInspector): +- """vmware VMDK format (monolithicSparse variant only) ++ """vmware VMDK format (monolithicSparse and streamOptimized variants only) + + This needs to store the 512 byte header and the descriptor region + which should be just after that. The descriptor region is some +@@ -582,7 +582,7 @@ + vmdktype = descriptor[type_idx:type_end] + else: + vmdktype = b'formatnotfound' +- if vmdktype != b'monolithicSparse': ++ if vmdktype not in (b'monolithicSparse', b'streamOptimized'): + LOG.warning('Unsupported VMDK format %s', vmdktype) + return 0 + +diff --git a/glance/tests/unit/common/test_format_inspector.py b/glance/tests/unit/common/test_format_inspector.py +index db6a983..38f8cae 100644 +--- a/glance/tests/unit/common/test_format_inspector.py ++++ b/glance/tests/unit/common/test_format_inspector.py +@@ -51,38 +51,51 @@ + except Exception: + pass + +- def _create_img(self, fmt, size): ++ def _create_img(self, fmt, size, subformat=None): + if fmt == 'vhd': + # QEMU calls the vhd format vpc + fmt = 'vpc' + +- fn = tempfile.mktemp(prefix='glance-unittest-formatinspector-', ++ opt = '' ++ prefix = 'glance-unittest-formatinspector-' ++ ++ if subformat: ++ opt = ' -o subformat=%s' % subformat ++ prefix += subformat + '-' ++ ++ fn = tempfile.mktemp(prefix=prefix, + suffix='.%s' % fmt) + self._created_files.append(fn) + subprocess.check_output( +- 'qemu-img create -f %s %s %i' % (fmt, fn, size), ++ 'qemu-img create -f %s %s %s %i' % (fmt, opt, fn, size), + shell=True) + return fn + +- def _create_allocated_vmdk(self, size_mb): ++ def _create_allocated_vmdk(self, size_mb, subformat=None): + # We need a "big" VMDK file to exercise some parts of the code of the + # format_inspector. A way to create one is to first create an empty + # file, and then to convert it with the -S 0 option. +- fn = tempfile.mktemp(prefix='glance-unittest-formatinspector-', +- suffix='.vmdk') +- self._created_files.append(fn) +- zeroes = tempfile.mktemp(prefix='glance-unittest-formatinspector-', +- suffix='.zero') +- self._created_files.append(zeroes) + +- # Create an empty file ++ if subformat is None: ++ # Matches qemu-img default, see `qemu-img convert -O vmdk -o help` ++ subformat = 'monolithicSparse' ++ ++ prefix = 'glance-unittest-formatinspector-%s-' % subformat ++ fn = tempfile.mktemp(prefix=prefix, suffix='.vmdk') ++ self._created_files.append(fn) ++ raw = tempfile.mktemp(prefix=prefix, suffix='.raw') ++ self._created_files.append(raw) ++ ++ # Create a file with pseudo-random data, otherwise it will get ++ # compressed in the streamOptimized format + subprocess.check_output( +- 'dd if=/dev/zero of=%s bs=1M count=%i' % (zeroes, size_mb), ++ 'dd if=/dev/urandom of=%s bs=1M count=%i' % (raw, size_mb), + shell=True) + + # Convert it to VMDK + subprocess.check_output( +- 'qemu-img convert -f raw -O vmdk -S 0 %s %s' % (zeroes, fn), ++ 'qemu-img convert -f raw -O vmdk -o subformat=%s -S 0 %s %s' % ( ++ subformat, raw, fn), + shell=True) + return fn + +@@ -101,8 +114,9 @@ + wrapper.close() + return fmt + +- def _test_format_at_image_size(self, format_name, image_size): +- img = self._create_img(format_name, image_size) ++ def _test_format_at_image_size(self, format_name, image_size, ++ subformat=None): ++ img = self._create_img(format_name, image_size, subformat=subformat) + + # Some formats have internal alignment restrictions making this not + # always exactly like image_size, so get the real value for comparison +@@ -124,11 +138,12 @@ + 'Format used more than 512KiB of memory: %s' % ( + fmt.context_info)) + +- def _test_format(self, format_name): ++ def _test_format(self, format_name, subformat=None): + # Try a few different image sizes, including some odd and very small + # sizes + for image_size in (512, 513, 2057, 7): +- self._test_format_at_image_size(format_name, image_size * units.Mi) ++ self._test_format_at_image_size(format_name, image_size * units.Mi, ++ subformat=subformat) + + def test_qcow2(self): + self._test_format('qcow2') +@@ -142,12 +157,15 @@ + def test_vmdk(self): + self._test_format('vmdk') + +- def test_vmdk_bad_descriptor_offset(self): ++ def test_vmdk_stream_optimized(self): ++ self._test_format('vmdk', 'streamOptimized') ++ ++ def _test_vmdk_bad_descriptor_offset(self, subformat=None): + format_name = 'vmdk' + image_size = 10 * units.Mi + descriptorOffsetAddr = 0x1c + BAD_ADDRESS = 0x400 +- img = self._create_img(format_name, image_size) ++ img = self._create_img(format_name, image_size, subformat=subformat) + + # Corrupt the header + fd = open(img, 'r+b') +@@ -167,7 +185,13 @@ + 'size %i block %i') % (format_name, image_size, + block_size)) + +- def test_vmdk_bad_descriptor_mem_limit(self): ++ def test_vmdk_bad_descriptor_offset(self): ++ self._test_vmdk_bad_descriptor_offset() ++ ++ def test_vmdk_bad_descriptor_offset_stream_optimized(self): ++ self._test_vmdk_bad_descriptor_offset(subformat='streamOptimized') ++ ++ def _test_vmdk_bad_descriptor_mem_limit(self, subformat=None): + format_name = 'vmdk' + image_size = 5 * units.Mi + virtual_size = 5 * units.Mi +@@ -176,7 +200,8 @@ + twoMBInSectors = (2 << 20) // 512 + # We need a big VMDK because otherwise we will not have enough data to + # fill-up the CaptureRegion. +- img = self._create_allocated_vmdk(image_size // units.Mi) ++ img = self._create_allocated_vmdk(image_size // units.Mi, ++ subformat=subformat) + + # Corrupt the end of descriptor address so it "ends" at 2MB + fd = open(img, 'r+b') +@@ -200,6 +225,12 @@ + 'Format used more than 1.5MiB of memory: %s' % ( + fmt.context_info)) + ++ def test_vmdk_bad_descriptor_mem_limit(self): ++ self._test_vmdk_bad_descriptor_mem_limit() ++ ++ def test_vmdk_bad_descriptor_mem_limit_stream_optimized(self): ++ self._test_vmdk_bad_descriptor_mem_limit(subformat='streamOptimized') ++ + def test_vdi(self): + self._test_format('vdi') + diff -Nru glance-25.1.0/debian/patches/CVE-2024-32498_3_1_glance-stable-2023.1.patch glance-25.1.0/debian/patches/CVE-2024-32498_3_1_glance-stable-2023.1.patch --- glance-25.1.0/debian/patches/CVE-2024-32498_3_1_glance-stable-2023.1.patch 1970-01-01 00:00:00.000000000 +0000 +++ glance-25.1.0/debian/patches/CVE-2024-32498_3_1_glance-stable-2023.1.patch 2024-06-21 08:38:56.000000000 +0000 @@ -0,0 +1,124 @@ +Description: CVE-2024-32498: [PATCH 1/7] Reject qcow files with data-file attributes +Author: Dan Smith +Date: Mon, 1 Apr 2024 08:06:31 -0700 +Change-Id: I6326a3e85c1ba4cb1da944a4323769f2399ed2c1 +Bug: https://launchpad.net/bugs/2059809 +Bug-Debian: https://bugs.debian.org/1074761 +Origin: upstream, https://review.opendev.org/c/openstack/glance/+/923277 +Last-Update: 2024-06-30 + +diff --git a/glance/async_/flows/base_import.py b/glance/async_/flows/base_import.py +index e6bb526b4..c0e2b7283 100644 +--- a/glance/async_/flows/base_import.py ++++ b/glance/async_/flows/base_import.py +@@ -181,6 +181,16 @@ class _ImportToFS(task.Task): + 'bfile': backing_file} + raise RuntimeError(msg) + ++ try: ++ data_file = metadata['format-specific']['data']['data-file'] ++ except KeyError: ++ data_file = None ++ if data_file is not None: ++ msg = _("File %(path)s has invalid data-file " ++ "%(dfile)s, aborting.") % {"path": path, ++ "dfile": data_file} ++ raise RuntimeError(msg) ++ + return path + + def revert(self, image_id, result, **kwargs): +diff --git a/glance/async_/flows/plugins/image_conversion.py b/glance/async_/flows/plugins/image_conversion.py +index e977764fa..4a9f754dc 100644 +--- a/glance/async_/flows/plugins/image_conversion.py ++++ b/glance/async_/flows/plugins/image_conversion.py +@@ -121,6 +121,14 @@ class _ConvertImage(task.Task): + raise RuntimeError( + 'QCOW images with backing files are not allowed') + ++ try: ++ data_file = metadata['format-specific']['data']['data-file'] ++ except KeyError: ++ data_file = None ++ if data_file is not None: ++ raise RuntimeError( ++ 'QCOW images with data-file set are not allowed') ++ + if metadata.get('format') == 'vmdk': + create_type = metadata.get( + 'format-specific', {}).get( +diff --git a/glance/tests/unit/async_/flows/plugins/test_image_conversion.py b/glance/tests/unit/async_/flows/plugins/test_image_conversion.py +index a60e2e1a5..bf8ca007d 100644 +--- a/glance/tests/unit/async_/flows/plugins/test_image_conversion.py ++++ b/glance/tests/unit/async_/flows/plugins/test_image_conversion.py +@@ -184,6 +184,22 @@ class TestConvertImageTask(test_utils.BaseTestCase): + self.assertEqual('QCOW images with backing files are not allowed', + str(e)) + ++ def test_image_convert_invalid_qcow_data_file(self): ++ data = {'format': 'qcow2', ++ 'format-specific': { ++ 'data': { ++ 'data-file': '/etc/hosts', ++ }, ++ }} ++ ++ convert = self._setup_image_convert_info_fail() ++ with mock.patch.object(processutils, 'execute') as exc_mock: ++ exc_mock.return_value = json.dumps(data), '' ++ e = self.assertRaises(RuntimeError, ++ convert.execute, 'file:///test/path.qcow') ++ self.assertEqual('QCOW images with data-file set are not allowed', ++ str(e)) ++ + def _test_image_convert_invalid_vmdk(self): + data = {'format': 'vmdk', + 'format-specific': { +diff --git a/glance/tests/unit/async_/flows/test_import.py b/glance/tests/unit/async_/flows/test_import.py +index 79f6b6de5..55d6f0928 100644 +--- a/glance/tests/unit/async_/flows/test_import.py ++++ b/glance/tests/unit/async_/flows/test_import.py +@@ -178,6 +178,39 @@ class TestImportTask(test_utils.BaseTestCase): + self.assertFalse(os.path.exists(tmp_image_path)) + self.assertTrue(os.path.exists(image_path)) + ++ def test_import_flow_invalid_data_file(self): ++ self.config(engine_mode='serial', ++ group='taskflow_executor') ++ ++ img_factory = mock.MagicMock() ++ ++ executor = taskflow_executor.TaskExecutor( ++ self.context, ++ self.task_repo, ++ self.img_repo, ++ img_factory) ++ ++ self.task_repo.get.return_value = self.task ++ ++ def create_image(*args, **kwargs): ++ kwargs['image_id'] = UUID1 ++ return self.img_factory.new_image(*args, **kwargs) ++ ++ self.img_repo.get.return_value = self.image ++ img_factory.new_image.side_effect = create_image ++ ++ with mock.patch.object(script_utils, 'get_image_data_iter') as dmock: ++ dmock.return_value = io.BytesIO(b"TEST_IMAGE") ++ ++ with mock.patch.object(putils, 'trycmd') as tmock: ++ out = json.dumps({'format-specific': ++ {'data': {'data-file': 'somefile'}}}) ++ tmock.return_value = (out, '') ++ e = self.assertRaises(RuntimeError, ++ executor.begin_processing, ++ self.task.task_id) ++ self.assertIn('somefile', str(e)) ++ + def test_import_flow_revert_import_to_fs(self): + self.config(engine_mode='serial', group='taskflow_executor') + +-- +2.41.0 + + diff -Nru glance-25.1.0/debian/patches/CVE-2024-32498_3_2_glance-stable-2023.1.patch glance-25.1.0/debian/patches/CVE-2024-32498_3_2_glance-stable-2023.1.patch --- glance-25.1.0/debian/patches/CVE-2024-32498_3_2_glance-stable-2023.1.patch 1970-01-01 00:00:00.000000000 +0000 +++ glance-25.1.0/debian/patches/CVE-2024-32498_3_2_glance-stable-2023.1.patch 2024-06-21 08:38:56.000000000 +0000 @@ -0,0 +1,325 @@ +Description: CVE-2024-32498: [PATCH 2/7] Extend format_inspector for QCOW safety + This adds two properties to the QcowInspector that makes it able to + indicate whether the file specifies a backing_file or data_file in the + header. Both conditions are considered unsafe for our usage. To + ease checking of this condition, a classmethod is added that takes + a local filename and digests just enough of the file to assert that + both conditions are false. +Author: Dan Smith +Date: Tue, 16 Apr 2024 10:29:10 -0700 +Change-Id: Iaf86b525397d41bd116999cabe0954a0a7efac65 +Bug: https://launchpad.net/bugs/2059809 +Bug-Debian: https://bugs.debian.org/1074761 +Origin: upstream, https://review.opendev.org/c/openstack/glance/+/923260 +Last-Update: 2024-06-30 + +diff --git a/glance/common/format_inspector.py b/glance/common/format_inspector.py +index d9576f1f8..32f048c3f 100755 +--- a/glance/common/format_inspector.py ++++ b/glance/common/format_inspector.py +@@ -28,6 +28,14 @@ from oslo_log import log as logging + LOG = logging.getLogger(__name__) + + ++def chunked_reader(fileobj, chunk_size=512): ++ while True: ++ chunk = fileobj.read(chunk_size) ++ if not chunk: ++ break ++ yield chunk ++ ++ + class CaptureRegion(object): + """Represents a region of a file we want to capture. + +@@ -176,10 +184,16 @@ class FileInspector(object): + @property + def actual_size(self): + """Returns the total size of the file, usually smaller than +- virtual_size. ++ virtual_size. NOTE: this will only be accurate if the entire ++ file is read and processed. + """ + return self._total_count + ++ @property ++ def complete(self): ++ """Returns True if we have all the information needed.""" ++ return all(r.complete for r in self._capture_regions.values()) ++ + def __str__(self): + """The string name of this file format.""" + return 'raw' +@@ -194,6 +208,35 @@ class FileInspector(object): + return {name: len(region.data) for name, region in + self._capture_regions.items()} + ++ @classmethod ++ def from_file(cls, filename): ++ """Read as much of a file as necessary to complete inspection. ++ ++ NOTE: Because we only read as much of the file as necessary, the ++ actual_size property will not reflect the size of the file, but the ++ amount of data we read before we satisfied the inspector. ++ ++ Raises ImageFormatError if we cannot parse the file. ++ """ ++ inspector = cls() ++ with open(filename, 'rb') as f: ++ for chunk in chunked_reader(f): ++ inspector.eat_chunk(chunk) ++ if inspector.complete: ++ # No need to eat any more data ++ break ++ if not inspector.complete or not inspector.format_match: ++ raise ImageFormatError('File is not in requested format') ++ return inspector ++ ++ def safety_check(self): ++ """Perform some checks to determine if this file is safe. ++ ++ Returns True if safe, False otherwise. It may raise ImageFormatError ++ if safety cannot be guaranteed because of parsing or other errors. ++ """ ++ return True ++ + + # The qcow2 format consists of a big-endian 72-byte header, of which + # only a small portion has information we care about: +@@ -202,15 +245,26 @@ class FileInspector(object): + # 0 0x00 Magic 4-bytes 'QFI\xfb' + # 4 0x04 Version (uint32_t, should always be 2 for modern files) + # . . . ++# 8 0x08 Backing file offset (uint64_t) + # 24 0x18 Size in bytes (unint64_t) ++# . . . ++# 72 0x48 Incompatible features bitfield (6 bytes) + # +-# https://people.gnome.org/~markmc/qcow-image-format.html ++# https://gitlab.com/qemu-project/qemu/-/blob/master/docs/interop/qcow2.txt + class QcowInspector(FileInspector): + """QEMU QCOW2 Format + + This should only require about 32 bytes of the beginning of the file +- to determine the virtual size. ++ to determine the virtual size, and 104 bytes to perform the safety check. + """ ++ ++ BF_OFFSET = 0x08 ++ BF_OFFSET_LEN = 8 ++ I_FEATURES = 0x48 ++ I_FEATURES_LEN = 8 ++ I_FEATURES_DATAFILE_BIT = 3 ++ I_FEATURES_MAX_BIT = 4 ++ + def __init__(self, *a, **k): + super(QcowInspector, self).__init__(*a, **k) + self.new_region('header', CaptureRegion(0, 512)) +@@ -220,6 +274,10 @@ class QcowInspector(FileInspector): + struct.unpack('>4sIQIIQ', self.region('header').data[:32])) + return magic, size + ++ @property ++ def has_header(self): ++ return self.region('header').complete ++ + @property + def virtual_size(self): + if not self.region('header').complete: +@@ -236,9 +294,77 @@ class QcowInspector(FileInspector): + magic, size = self._qcow_header_data() + return magic == b'QFI\xFB' + ++ @property ++ def has_backing_file(self): ++ if not self.region('header').complete: ++ return None ++ if not self.format_match: ++ return False ++ bf_offset_bytes = self.region('header').data[ ++ self.BF_OFFSET:self.BF_OFFSET + self.BF_OFFSET_LEN] ++ # nonzero means "has a backing file" ++ bf_offset, = struct.unpack('>Q', bf_offset_bytes) ++ return bf_offset != 0 ++ ++ @property ++ def has_unknown_features(self): ++ if not self.region('header').complete: ++ return None ++ if not self.format_match: ++ return False ++ i_features = self.region('header').data[ ++ self.I_FEATURES:self.I_FEATURES + self.I_FEATURES_LEN] ++ ++ # This is the maximum byte number we should expect any bits to be set ++ max_byte = self.I_FEATURES_MAX_BIT // 8 ++ ++ # The flag bytes are in big-endian ordering, so if we process ++ # them in index-order, they're reversed ++ for i, byte_num in enumerate(reversed(range(self.I_FEATURES_LEN))): ++ if byte_num == max_byte: ++ # If we're in the max-allowed byte, allow any bits less than ++ # the maximum-known feature flag bit to be set ++ allow_mask = ((1 << self.I_FEATURES_MAX_BIT) - 1) ++ elif byte_num > max_byte: ++ # If we're above the byte with the maximum known feature flag ++ # bit, then we expect all zeroes ++ allow_mask = 0x0 ++ else: ++ # Any earlier-than-the-maximum byte can have any of the flag ++ # bits set ++ allow_mask = 0xFF ++ ++ if i_features[i] & ~allow_mask: ++ LOG.warning('Found unknown feature bit in byte %i: %s/%s', ++ byte_num, bin(i_features[byte_num] & ~allow_mask), ++ bin(allow_mask)) ++ return True ++ ++ return False ++ ++ @property ++ def has_data_file(self): ++ if not self.region('header').complete: ++ return None ++ if not self.format_match: ++ return False ++ i_features = self.region('header').data[ ++ self.I_FEATURES:self.I_FEATURES + self.I_FEATURES_LEN] ++ ++ # First byte of bitfield, which is i_features[7] ++ byte = self.I_FEATURES_LEN - 1 - self.I_FEATURES_DATAFILE_BIT // 8 ++ # Third bit of bitfield, which is 0x04 ++ bit = 1 << (self.I_FEATURES_DATAFILE_BIT - 1 % 8) ++ return bool(i_features[byte] & bit) ++ + def __str__(self): + return 'qcow2' + ++ def safety_check(self): ++ return (not self.has_backing_file and ++ not self.has_data_file and ++ not self.has_unknown_features) ++ + + # The VHD (or VPC as QEMU calls it) format consists of a big-endian + # 512-byte "footer" at the beginning of the file with various +diff --git a/glance/tests/unit/common/test_format_inspector.py b/glance/tests/unit/common/test_format_inspector.py +index 38f8caeb4..359cf97ba 100644 +--- a/glance/tests/unit/common/test_format_inspector.py ++++ b/glance/tests/unit/common/test_format_inspector.py +@@ -51,18 +51,28 @@ class TestFormatInspectors(test_utils.BaseTestCase): + except Exception: + pass + +- def _create_img(self, fmt, size, subformat=None): ++ def _create_img(self, fmt, size, subformat=None, options=None, ++ backing_file=None): + if fmt == 'vhd': + # QEMU calls the vhd format vpc + fmt = 'vpc' + ++ if options is None: ++ options = {} + opt = '' + prefix = 'glance-unittest-formatinspector-' + + if subformat: +- opt = ' -o subformat=%s' % subformat ++ options['subformat'] = subformat + prefix += subformat + '-' + ++ if options: ++ opt += '-o ' + ','.join('%s=%s' % (k, v) ++ for k, v in options.items()) ++ ++ if backing_file is not None: ++ opt += ' -b %s -F raw' % backing_file ++ + fn = tempfile.mktemp(prefix=prefix, + suffix='.%s' % fmt) + self._created_files.append(fn) +@@ -160,6 +170,15 @@ class TestFormatInspectors(test_utils.BaseTestCase): + def test_vmdk_stream_optimized(self): + self._test_format('vmdk', 'streamOptimized') + ++ def test_from_file_reads_minimum(self): ++ img = self._create_img('qcow2', 10 * units.Mi) ++ file_size = os.stat(img).st_size ++ fmt = format_inspector.QcowInspector.from_file(img) ++ # We know everything we need from the first 512 bytes of a QCOW image, ++ # so make sure that we did not read the whole thing when we inspect ++ # a local file. ++ self.assertLess(fmt.actual_size, file_size) ++ + def _test_vmdk_bad_descriptor_offset(self, subformat=None): + format_name = 'vmdk' + image_size = 10 * units.Mi +@@ -231,6 +250,63 @@ class TestFormatInspectors(test_utils.BaseTestCase): + def test_vmdk_bad_descriptor_mem_limit_stream_optimized(self): + self._test_vmdk_bad_descriptor_mem_limit(subformat='streamOptimized') + ++ def test_qcow2_safety_checks(self): ++ # Create backing and data-file names (and initialize the backing file) ++ backing_fn = tempfile.mktemp(prefix='backing') ++ self._created_files.append(backing_fn) ++ with open(backing_fn, 'w') as f: ++ f.write('foobar') ++ data_fn = tempfile.mktemp(prefix='data') ++ self._created_files.append(data_fn) ++ ++ # A qcow with no backing or data file is safe ++ fn = self._create_img('qcow2', 5 * units.Mi, None) ++ inspector = format_inspector.QcowInspector.from_file(fn) ++ self.assertTrue(inspector.safety_check()) ++ ++ # A backing file makes it unsafe ++ fn = self._create_img('qcow2', 5 * units.Mi, None, ++ backing_file=backing_fn) ++ inspector = format_inspector.QcowInspector.from_file(fn) ++ self.assertFalse(inspector.safety_check()) ++ ++ # A data-file makes it unsafe ++ fn = self._create_img('qcow2', 5 * units.Mi, ++ options={'data_file': data_fn, ++ 'data_file_raw': 'on'}) ++ inspector = format_inspector.QcowInspector.from_file(fn) ++ self.assertFalse(inspector.safety_check()) ++ ++ # Trying to load a non-QCOW file is an error ++ self.assertRaises(format_inspector.ImageFormatError, ++ format_inspector.QcowInspector.from_file, ++ backing_fn) ++ ++ def test_qcow2_feature_flag_checks(self): ++ data = bytearray(512) ++ data[0:4] = b'QFI\xFB' ++ inspector = format_inspector.QcowInspector() ++ inspector.region('header').data = data ++ ++ # All zeros, no feature flags - all good ++ self.assertFalse(inspector.has_unknown_features) ++ ++ # A feature flag set in the first byte (highest-order) is not ++ # something we know about, so fail. ++ data[0x48] = 0x01 ++ self.assertTrue(inspector.has_unknown_features) ++ ++ # The first bit in the last byte (lowest-order) is known (the dirty ++ # bit) so that should pass ++ data[0x48] = 0x00 ++ data[0x4F] = 0x01 ++ self.assertFalse(inspector.has_unknown_features) ++ ++ # Currently (as of 2024), the high-order feature flag bit in the low- ++ # order byte is not assigned, so make sure we reject it. ++ data[0x4F] = 0x80 ++ self.assertTrue(inspector.has_unknown_features) ++ + def test_vdi(self): + self._test_format('vdi') + +-- +2.41.0 + + diff -Nru glance-25.1.0/debian/patches/CVE-2024-32498_3_3_glance-stable-2023.1.patch glance-25.1.0/debian/patches/CVE-2024-32498_3_3_glance-stable-2023.1.patch --- glance-25.1.0/debian/patches/CVE-2024-32498_3_3_glance-stable-2023.1.patch 1970-01-01 00:00:00.000000000 +0000 +++ glance-25.1.0/debian/patches/CVE-2024-32498_3_3_glance-stable-2023.1.patch 2024-06-21 08:38:56.000000000 +0000 @@ -0,0 +1,183 @@ +Description: CVE-2024-32498: [PATCH 3/7] 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. +Author: Dan Smith +Date: Thu, 18 Apr 2024 14:51:52 -0700 +Change-Id: I4a1c6dff7854c49940a0ac7988722aa6befc04fa +Bug: https://launchpad.net/bugs/2059809 +Bug-Debian: https://bugs.debian.org/1074761 +Origin: upstream, https://review.opendev.org/c/openstack/glance/+/923279 +Last-Update: 2024-06-30 + +diff --git a/glance/common/format_inspector.py b/glance/common/format_inspector.py +index 32f048c3f..a11ff1a5e 100755 +--- a/glance/common/format_inspector.py ++++ b/glance/common/format_inspector.py +@@ -650,6 +650,7 @@ class VMDKInspector(FileInspector): + # at 0x200 and 1MB - 1 + DESC_OFFSET = 0x200 + DESC_MAX_SIZE = (1 << 20) - 1 ++ GD_AT_END = 0xffffffffffffffff + + def __init__(self, *a, **k): + super(VMDKInspector, self).__init__(*a, **k) +@@ -662,8 +663,9 @@ class VMDKInspector(FileInspector): + if not self.region('header').complete: + return + +- sig, ver, _flags, _sectors, _grain, desc_sec, desc_num = struct.unpack( +- '<4sIIQQQQ', self.region('header').data[:44]) ++ (sig, ver, _flags, _sectors, _grain, desc_sec, desc_num, ++ _numGTEsperGT, _rgdOffset, gdOffset) = struct.unpack( ++ '<4sIIQQQQIQQ', self.region('header').data[:64]) + + if sig != b'KDMV': + raise ImageFormatError('Signature KDMV not found: %r' % sig) +@@ -671,6 +673,11 @@ class VMDKInspector(FileInspector): + if ver not in (1, 2, 3): + raise ImageFormatError('Unsupported format version %i' % ver) + ++ if gdOffset == self.GD_AT_END: ++ # This means we have a footer, which takes precedence over the ++ # header, which we cannot support since we stream. ++ raise ImageFormatError('Unsupported VMDK footer') ++ + # Since we parse both desc_sec and desc_num (the location of the + # VMDK's descriptor, expressed in 512 bytes sectors) we enforce a + # check on the bounds to create a reasonable CaptureRegion. This +@@ -718,6 +725,59 @@ class VMDKInspector(FileInspector): + + return sectors * 512 + ++ def safety_check(self): ++ if (not self.has_region('descriptor') or ++ not self.region('descriptor').complete): ++ return False ++ ++ try: ++ # Descriptor is padded to 512 bytes ++ desc_data = self.region('descriptor').data.rstrip(b'\x00') ++ # Descriptor is actually case-insensitive ASCII text ++ desc_text = desc_data.decode('ascii').lower() ++ except UnicodeDecodeError: ++ LOG.error('VMDK descriptor failed to decode as ASCII') ++ raise ImageFormatError('Invalid VMDK descriptor data') ++ ++ extent_access = ('rw', 'rdonly', 'noaccess') ++ header_fields = [] ++ extents = [] ++ ddb = [] ++ ++ # NOTE(danms): Cautiously parse the VMDK descriptor. Each line must ++ # be something we understand, otherwise we refuse it. ++ for line in [x.strip() for x in desc_text.split('\n')]: ++ if line.startswith('#') or not line: ++ # Blank or comment lines are ignored ++ continue ++ elif line.startswith('ddb'): ++ # DDB lines are allowed (but not used by us) ++ ddb.append(line) ++ elif '=' in line and ' ' not in line.split('=')[0]: ++ # Header fields are a single word followed by an '=' and some ++ # value ++ header_fields.append(line) ++ elif line.split(' ')[0] in extent_access: ++ # Extent lines start with one of the three access modes ++ extents.append(line) ++ else: ++ # Anything else results in a rejection ++ LOG.error('Unsupported line %r in VMDK descriptor', line) ++ raise ImageFormatError('Invalid VMDK descriptor data') ++ ++ # Check all the extent lines for concerning content ++ for extent_line in extents: ++ if '/' in extent_line: ++ LOG.error('Extent line %r contains unsafe characters', ++ extent_line) ++ return False ++ ++ if not extents: ++ LOG.error('VMDK file specified no extents') ++ return False ++ ++ return True ++ + def __str__(self): + return 'vmdk' + +diff --git a/glance/tests/unit/common/test_format_inspector.py b/glance/tests/unit/common/test_format_inspector.py +index 359cf97ba..9b458f970 100644 +--- a/glance/tests/unit/common/test_format_inspector.py ++++ b/glance/tests/unit/common/test_format_inspector.py +@@ -307,6 +307,62 @@ class TestFormatInspectors(test_utils.BaseTestCase): + data[0x4F] = 0x80 + self.assertTrue(inspector.has_unknown_features) + ++ def test_vmdk_safety_checks(self): ++ region = format_inspector.CaptureRegion(0, 0) ++ inspector = format_inspector.VMDKInspector() ++ inspector.new_region('descriptor', region) ++ ++ # This should be a legit VMDK descriptor which comments, blank lines, ++ # an extent, some ddb content, and some header values. ++ legit_desc = ['# This is a comment', ++ '', ++ ' ', ++ 'createType=monolithicSparse', ++ 'RW 1234 SPARSE "foo.vmdk"', ++ 'ddb.adapterType = "MFM', ++ '# EOF'] ++ region.data = ('\n'.join(legit_desc)).encode('ascii') ++ region.length = len(region.data) ++ self.assertTrue(inspector.safety_check()) ++ ++ # Any of these lines should trigger an error indicating that there is ++ # something in the descriptor we don't understand ++ bad_lines = [ ++ '#\U0001F4A9', ++ 'header Name=foo', ++ 'foo bar', ++ 'WR 123 SPARSE "foo.vmdk"', ++ ] ++ ++ for bad_line in bad_lines: ++ # Encode as UTF-8 purely so we can test that anything non-ASCII ++ # will trigger the decode check ++ region.data = bad_line.encode('utf-8') ++ region.length = len(region.data) ++ self.assertRaisesRegex(format_inspector.ImageFormatError, ++ 'Invalid VMDK descriptor', ++ inspector.safety_check) ++ ++ # Extents with slashes in the name fail the safety check ++ region.data = b'RW 123 SPARSE "/etc/shadow"' ++ region.length = len(region.data) ++ self.assertFalse(inspector.safety_check()) ++ ++ # A descriptor that specifies no extents fails the safety check ++ region.data = b'# Nothing' ++ region.length = len(region.data) ++ self.assertFalse(inspector.safety_check()) ++ ++ def test_vmdk_reject_footer(self): ++ data = struct.pack('<4sIIQQQQIQQ', b'KDMV', 3, 0, 0, 0, 0, 1, 0, 0, ++ format_inspector.VMDKInspector.GD_AT_END) ++ inspector = format_inspector.VMDKInspector() ++ inspector.region('header').data = data ++ inspector.region('header').length = len(data) ++ self.assertRaisesRegex(format_inspector.ImageFormatError, ++ 'footer', ++ inspector.post_process) ++ + def test_vdi(self): + self._test_format('vdi') + +-- +2.41.0 + + diff -Nru glance-25.1.0/debian/patches/CVE-2024-32498_3_4_glance-stable-2023.1.patch glance-25.1.0/debian/patches/CVE-2024-32498_3_4_glance-stable-2023.1.patch --- glance-25.1.0/debian/patches/CVE-2024-32498_3_4_glance-stable-2023.1.patch 1970-01-01 00:00:00.000000000 +0000 +++ glance-25.1.0/debian/patches/CVE-2024-32498_3_4_glance-stable-2023.1.patch 2024-06-21 08:38:56.000000000 +0000 @@ -0,0 +1,262 @@ +Description: CVE-2024-32498: [PATCH 4/7] Reject unsafe qcow and vmdk files + This causes us to use the format inspector to pre-examine qcow and + vmdk files for safe configurations before even using qemu-img + on them. +Author: Dan Smith +Date: Tue, 16 Apr 2024 11:20:48 -0700 +Change-Id: I0554706368e573e11f649c09569f7c21cbc8634b +Bug: https://launchpad.net/bugs/2059809 +Bug-Debian: https://bugs.debian.org/1074761 +Origin: upstream, https://review.opendev.org/c/openstack/glance/+/923280 +Last-Update: 2024-06-30 + +diff --git a/glance/async_/flows/plugins/image_conversion.py b/glance/async_/flows/plugins/image_conversion.py +index 4a9f754dc..6f5199c82 100644 +--- a/glance/async_/flows/plugins/image_conversion.py ++++ b/glance/async_/flows/plugins/image_conversion.py +@@ -25,6 +25,7 @@ from taskflow.patterns import linear_flow as lf + from taskflow import task + + from glance.async_ import utils ++from glance.common import format_inspector + from glance.i18n import _, _LI + + LOG = logging.getLogger(__name__) +@@ -87,8 +88,40 @@ class _ConvertImage(task.Task): + 'target': target_format} + self.dest_path = dest_path + ++ source_format = action.image_disk_format ++ inspector_cls = format_inspector.get_inspector(source_format) ++ if not inspector_cls: ++ # We cannot convert from disk_format types that qemu-img doesn't ++ # support (like iso, ploop, etc). The ones it supports overlaps ++ # with the ones we have inspectors for, so reject conversion for ++ # any format we don't have an inspector for. ++ raise RuntimeError( ++ 'Unable to convert from format %s' % source_format) ++ ++ # Use our own cautious inspector module (if we have one for this ++ # format) to make sure a file is the format the submitter claimed ++ # it is and that it passes some basic safety checks _before_ we run ++ # qemu-img on it. ++ # See https://bugs.launchpad.net/nova/+bug/2059809 for details. ++ try: ++ inspector = inspector_cls.from_file(src_path) ++ if not inspector.safety_check(): ++ LOG.error('Image failed %s safety check; aborting conversion', ++ source_format) ++ raise RuntimeError('Image has disallowed configuration') ++ except RuntimeError: ++ raise ++ except format_inspector.ImageFormatError as e: ++ LOG.error('Image claimed to be %s format failed format ' ++ 'inspection: %s', source_format, e) ++ raise RuntimeError('Image format detection failed') ++ except Exception as e: ++ LOG.exception('Unknown error inspecting image format: %s', e) ++ raise RuntimeError('Unable to inspect image') ++ + try: + stdout, stderr = putils.trycmd("qemu-img", "info", ++ "-f", source_format, + "--output=json", + src_path, + prlimit=utils.QEMU_IMG_PROC_LIMITS, +@@ -105,13 +138,10 @@ class _ConvertImage(task.Task): + raise RuntimeError(stderr) + + metadata = json.loads(stdout) +- try: +- source_format = metadata['format'] +- except KeyError: +- msg = ("Failed to do introspection as part of image " +- "conversion for %(iid)s: Source format not reported") +- LOG.error(msg, {'iid': self.image_id}) +- raise RuntimeError(msg) ++ if metadata.get('format') != source_format: ++ LOG.error('Image claiming to be %s reported as %s by qemu-img', ++ source_format, metadata.get('format', 'unknown')) ++ raise RuntimeError('Image metadata disagrees about format') + + virtual_size = metadata.get('virtual-size', 0) + action.set_image_attribute(virtual_size=virtual_size) +diff --git a/glance/tests/unit/async_/flows/plugins/test_image_conversion.py b/glance/tests/unit/async_/flows/plugins/test_image_conversion.py +index bf8ca007d..1942dcc43 100644 +--- a/glance/tests/unit/async_/flows/plugins/test_image_conversion.py ++++ b/glance/tests/unit/async_/flows/plugins/test_image_conversion.py +@@ -13,6 +13,7 @@ + # License for the specific language governing permissions and limitations + # under the License. + ++import fixtures + import json + import os + from unittest import mock +@@ -24,6 +25,7 @@ from oslo_config import cfg + import glance.async_.flows.api_image_import as import_flow + import glance.async_.flows.plugins.image_conversion as image_conversion + from glance.async_ import utils as async_utils ++from glance.common import format_inspector + from glance.common import utils + from glance import domain + from glance import gateway +@@ -90,6 +92,11 @@ class TestConvertImageTask(test_utils.BaseTestCase): + self.image_id, + self.task.task_id) + ++ self.inspector_mock = mock.MagicMock() ++ self.useFixture(fixtures.MockPatch('glance.common.format_inspector.' ++ 'get_inspector', ++ self.inspector_mock)) ++ + @mock.patch.object(os, 'stat') + @mock.patch.object(os, 'remove') + def test_image_convert_success(self, mock_os_remove, mock_os_stat): +@@ -104,7 +111,7 @@ class TestConvertImageTask(test_utils.BaseTestCase): + image = mock.MagicMock(image_id=self.image_id, virtual_size=None, + extra_properties={ + 'os_glance_import_task': self.task.task_id}, +- disk_format='qcow2') ++ disk_format='raw') + self.img_repo.get.return_value = image + + with mock.patch.object(processutils, 'execute') as exc_mock: +@@ -126,7 +133,7 @@ class TestConvertImageTask(test_utils.BaseTestCase): + self.assertEqual(456, image.virtual_size) + self.assertEqual(123, image.size) + +- def _setup_image_convert_info_fail(self): ++ def _setup_image_convert_info_fail(self, disk_format='qcow2'): + image_convert = image_conversion._ConvertImage(self.context, + self.task.task_id, + self.task_type, +@@ -136,7 +143,7 @@ class TestConvertImageTask(test_utils.BaseTestCase): + image = mock.MagicMock(image_id=self.image_id, virtual_size=None, + extra_properties={ + 'os_glance_import_task': self.task.task_id}, +- disk_format='qcow2') ++ disk_format=disk_format) + self.img_repo.get.return_value = image + return image_convert + +@@ -148,6 +155,7 @@ class TestConvertImageTask(test_utils.BaseTestCase): + convert.execute, 'file:///test/path.raw') + exc_mock.assert_called_once_with( + 'qemu-img', 'info', ++ '-f', 'qcow2', + '--output=json', + '/test/path.raw', + prlimit=async_utils.QEMU_IMG_PROC_LIMITS, +@@ -164,6 +172,7 @@ class TestConvertImageTask(test_utils.BaseTestCase): + convert.execute, 'file:///test/path.raw') + exc_mock.assert_called_once_with( + 'qemu-img', 'info', ++ '-f', 'qcow2', + '--output=json', + '/test/path.raw', + prlimit=async_utils.QEMU_IMG_PROC_LIMITS, +@@ -200,6 +209,36 @@ class TestConvertImageTask(test_utils.BaseTestCase): + self.assertEqual('QCOW images with data-file set are not allowed', + str(e)) + ++ def test_image_convert_no_inspector_match(self): ++ convert = self._setup_image_convert_info_fail() ++ self.inspector_mock.return_value = None ++ self.assertRaisesRegex(RuntimeError, ++ 'Unable to convert from format', ++ convert.execute, 'file:///test/path.hpfs') ++ ++ def test_image_convert_fails_inspection_safety_check(self): ++ convert = self._setup_image_convert_info_fail() ++ inspector = self.inspector_mock.return_value.from_file.return_value ++ inspector.safety_check.return_value = False ++ self.assertRaisesRegex(RuntimeError, ++ 'Image has disallowed configuration', ++ convert.execute, 'file:///test/path.qcow') ++ ++ def test_image_convert_fails_inspection_format_check(self): ++ convert = self._setup_image_convert_info_fail() ++ self.inspector_mock.return_value.from_file.side_effect = ( ++ format_inspector.ImageFormatError()) ++ self.assertRaisesRegex(RuntimeError, ++ 'Image format detection failed', ++ convert.execute, 'file:///test/path.qcow') ++ ++ def test_image_convert_fails_inspection_error(self): ++ convert = self._setup_image_convert_info_fail() ++ self.inspector_mock.return_value.from_file.side_effect = ValueError ++ self.assertRaisesRegex(RuntimeError, ++ 'Unable to inspect image', ++ convert.execute, 'file:///test/path.qcow') ++ + def _test_image_convert_invalid_vmdk(self): + data = {'format': 'vmdk', + 'format-specific': { +@@ -207,7 +246,7 @@ class TestConvertImageTask(test_utils.BaseTestCase): + 'create-type': 'monolithicFlat', + }}} + +- convert = self._setup_image_convert_info_fail() ++ convert = self._setup_image_convert_info_fail(disk_format='vmdk') + with mock.patch.object(processutils, 'execute') as exc_mock: + exc_mock.return_value = json.dumps(data), '' + convert.execute('file:///test/path.vmdk') +@@ -236,7 +275,7 @@ class TestConvertImageTask(test_utils.BaseTestCase): + self._test_image_convert_invalid_vmdk) + + def test_image_convert_fails(self): +- convert = self._setup_image_convert_info_fail() ++ convert = self._setup_image_convert_info_fail(disk_format='raw') + with mock.patch.object(processutils, 'execute') as exc_mock: + exc_mock.side_effect = [('{"format":"raw"}', ''), + OSError('convert_fail')] +@@ -244,6 +283,7 @@ class TestConvertImageTask(test_utils.BaseTestCase): + convert.execute, 'file:///test/path.raw') + exc_mock.assert_has_calls( + [mock.call('qemu-img', 'info', ++ '-f', 'raw', + '--output=json', + '/test/path.raw', + prlimit=async_utils.QEMU_IMG_PROC_LIMITS, +@@ -256,7 +296,7 @@ class TestConvertImageTask(test_utils.BaseTestCase): + self.img_repo.save.assert_not_called() + + def test_image_convert_reports_fail(self): +- convert = self._setup_image_convert_info_fail() ++ convert = self._setup_image_convert_info_fail(disk_format='raw') + with mock.patch.object(processutils, 'execute') as exc_mock: + exc_mock.side_effect = [('{"format":"raw"}', ''), + ('', 'some error')] +@@ -264,6 +304,7 @@ class TestConvertImageTask(test_utils.BaseTestCase): + convert.execute, 'file:///test/path.raw') + exc_mock.assert_has_calls( + [mock.call('qemu-img', 'info', ++ '-f', 'raw', + '--output=json', + '/test/path.raw', + prlimit=async_utils.QEMU_IMG_PROC_LIMITS, +@@ -281,9 +322,10 @@ class TestConvertImageTask(test_utils.BaseTestCase): + exc_mock.return_value = ('{}', '') + exc = self.assertRaises(RuntimeError, + convert.execute, 'file:///test/path.raw') +- self.assertIn('Source format not reported', str(exc)) ++ self.assertIn('Image metadata disagrees about format', str(exc)) + exc_mock.assert_called_once_with( + 'qemu-img', 'info', ++ '-f', 'qcow2', + '--output=json', + '/test/path.raw', + prlimit=async_utils.QEMU_IMG_PROC_LIMITS, +@@ -301,6 +343,7 @@ class TestConvertImageTask(test_utils.BaseTestCase): + # Make sure we only called qemu-img for inspection, not conversion + exc_mock.assert_called_once_with( + 'qemu-img', 'info', ++ '-f', 'qcow2', + '--output=json', + '/test/path.qcow', + prlimit=async_utils.QEMU_IMG_PROC_LIMITS, +-- +2.41.0 + + diff -Nru glance-25.1.0/debian/patches/CVE-2024-32498_3_5_glance-stable-2023.1.patch glance-25.1.0/debian/patches/CVE-2024-32498_3_5_glance-stable-2023.1.patch --- glance-25.1.0/debian/patches/CVE-2024-32498_3_5_glance-stable-2023.1.patch 1970-01-01 00:00:00.000000000 +0000 +++ glance-25.1.0/debian/patches/CVE-2024-32498_3_5_glance-stable-2023.1.patch 2024-06-21 08:38:56.000000000 +0000 @@ -0,0 +1,69 @@ +Description: CVE-2024-32498: [PATCH 5/7] Add QED format detection to format_inspector + This merely recognizes this format and always marks it as unsafe + because no service supports it. This prevents someone from uploading + one that we will ask qemu-img to inspect. +Author: Dan Smith +Date: Thu, 27 Jun 2024 09:33:55 -0700 +Change-Id: Ieea7b7eb0f380571bd4937cded920776e05f7ec4 +Bug: https://launchpad.net/bugs/2059809 +Bug-Debian: https://bugs.debian.org/1074761 +Origin: upstream, https://review.opendev.org/c/openstack/glance/+/923281 +Last-Update: 2024-06-30 + +diff --git a/glance/common/format_inspector.py b/glance/common/format_inspector.py +index a11ff1a5e..4d5e4fa45 100755 +--- a/glance/common/format_inspector.py ++++ b/glance/common/format_inspector.py +@@ -366,6 +366,23 @@ class QcowInspector(FileInspector): + not self.has_unknown_features) + + ++class QEDInspector(FileInspector): ++ def __init__(self, tracing=False): ++ super().__init__(tracing) ++ self.new_region('header', CaptureRegion(0, 512)) ++ ++ @property ++ def format_match(self): ++ if not self.region('header').complete: ++ return False ++ return self.region('header').data.startswith(b'QED\x00') ++ ++ def safety_check(self): ++ # QED format is not supported by anyone, but we want to detect it ++ # and mark it as just always unsafe. ++ return False ++ ++ + # The VHD (or VPC as QEMU calls it) format consists of a big-endian + # 512-byte "footer" at the beginning of the file with various + # information, most of which does not matter to us: +@@ -879,6 +896,7 @@ def get_inspector(format_name): + 'vhdx': VHDXInspector, + 'vmdk': VMDKInspector, + 'vdi': VDIInspector, ++ 'qed': QEDInspector, + } + + return formats.get(format_name) +diff --git a/glance/tests/unit/common/test_format_inspector.py b/glance/tests/unit/common/test_format_inspector.py +index 9b458f970..9d4a7cb9e 100644 +--- a/glance/tests/unit/common/test_format_inspector.py ++++ b/glance/tests/unit/common/test_format_inspector.py +@@ -179,6 +179,12 @@ class TestFormatInspectors(test_utils.BaseTestCase): + # a local file. + self.assertLess(fmt.actual_size, file_size) + ++ def test_qed_always_unsafe(self): ++ img = self._create_img('qed', 10 * units.Mi) ++ fmt = format_inspector.get_inspector('qed').from_file(img) ++ self.assertTrue(fmt.format_match) ++ self.assertFalse(fmt.safety_check()) ++ + def _test_vmdk_bad_descriptor_offset(self, subformat=None): + format_name = 'vmdk' + image_size = 10 * units.Mi +-- +2.41.0 + + diff -Nru glance-25.1.0/debian/patches/CVE-2024-32498_3_6_glance-stable-2023.1.patch glance-25.1.0/debian/patches/CVE-2024-32498_3_6_glance-stable-2023.1.patch --- glance-25.1.0/debian/patches/CVE-2024-32498_3_6_glance-stable-2023.1.patch 1970-01-01 00:00:00.000000000 +0000 +++ glance-25.1.0/debian/patches/CVE-2024-32498_3_6_glance-stable-2023.1.patch 2024-06-21 08:38:56.000000000 +0000 @@ -0,0 +1,148 @@ +Description: CVE-2024-32498: [PATCH 6/7] Add file format detection to format_inspector +Author: Dan Smith +Date: Thu, 18 Apr 2024 08:25:24 -0700 +Change-Id: If0a4251465507be035ffaf9d855299611637cfa9 +Bug: https://launchpad.net/bugs/2059809 +Bug-Debian: https://bugs.debian.org/1074761 +Origin: upstream, https://review.opendev.org/c/openstack/glance/+/923282 +Last-Update: 2024-06-30 + +diff --git a/glance/common/format_inspector.py b/glance/common/format_inspector.py +index 4d5e4fa45..65502d889 100755 +--- a/glance/common/format_inspector.py ++++ b/glance/common/format_inspector.py +@@ -883,20 +883,52 @@ class InfoWrapper(object): + self._source.close() + + ++ALL_FORMATS = { ++ 'raw': FileInspector, ++ 'qcow2': QcowInspector, ++ 'vhd': VHDInspector, ++ 'vhdx': VHDXInspector, ++ 'vmdk': VMDKInspector, ++ 'vdi': VDIInspector, ++ 'qed': QEDInspector, ++} ++ ++ + def get_inspector(format_name): + """Returns a FormatInspector class based on the given name. + + :param format_name: The name of the disk_format (raw, qcow2, etc). + :returns: A FormatInspector or None if unsupported. + """ +- formats = { +- 'raw': FileInspector, +- 'qcow2': QcowInspector, +- 'vhd': VHDInspector, +- 'vhdx': VHDXInspector, +- 'vmdk': VMDKInspector, +- 'vdi': VDIInspector, +- 'qed': QEDInspector, +- } +- +- return formats.get(format_name) ++ ++ return ALL_FORMATS.get(format_name) ++ ++ ++def detect_file_format(filename): ++ """Attempts to detect the format of a file. ++ ++ This runs through a file one time, running all the known inspectors in ++ parallel. It stops reading the file once one of them matches or all of ++ them are sure they don't match. ++ ++ Returns the FileInspector that matched, if any. None if 'raw'. ++ """ ++ inspectors = {k: v() for k, v in ALL_FORMATS.items()} ++ with open(filename, 'rb') as f: ++ for chunk in chunked_reader(f): ++ for format, inspector in list(inspectors.items()): ++ try: ++ inspector.eat_chunk(chunk) ++ except ImageFormatError: ++ # No match, so stop considering this format ++ inspectors.pop(format) ++ continue ++ if (inspector.format_match and inspector.complete and ++ format != 'raw'): ++ # First complete match (other than raw) wins ++ return inspector ++ if all(i.complete for i in inspectors.values()): ++ # If all the inspectors are sure they are not a match, avoid ++ # reading to the end of the file to settle on 'raw'. ++ break ++ return inspectors['raw'] +diff --git a/glance/tests/unit/common/test_format_inspector.py b/glance/tests/unit/common/test_format_inspector.py +index 9d4a7cb9e..fce9a1a97 100644 +--- a/glance/tests/unit/common/test_format_inspector.py ++++ b/glance/tests/unit/common/test_format_inspector.py +@@ -313,62 +313,6 @@ class TestFormatInspectors(test_utils.BaseTestCase): + data[0x4F] = 0x80 + self.assertTrue(inspector.has_unknown_features) + +- def test_vmdk_safety_checks(self): +- region = format_inspector.CaptureRegion(0, 0) +- inspector = format_inspector.VMDKInspector() +- inspector.new_region('descriptor', region) +- +- # This should be a legit VMDK descriptor which comments, blank lines, +- # an extent, some ddb content, and some header values. +- legit_desc = ['# This is a comment', +- '', +- ' ', +- 'createType=monolithicSparse', +- 'RW 1234 SPARSE "foo.vmdk"', +- 'ddb.adapterType = "MFM', +- '# EOF'] +- region.data = ('\n'.join(legit_desc)).encode('ascii') +- region.length = len(region.data) +- self.assertTrue(inspector.safety_check()) +- +- # Any of these lines should trigger an error indicating that there is +- # something in the descriptor we don't understand +- bad_lines = [ +- '#\U0001F4A9', +- 'header Name=foo', +- 'foo bar', +- 'WR 123 SPARSE "foo.vmdk"', +- ] +- +- for bad_line in bad_lines: +- # Encode as UTF-8 purely so we can test that anything non-ASCII +- # will trigger the decode check +- region.data = bad_line.encode('utf-8') +- region.length = len(region.data) +- self.assertRaisesRegex(format_inspector.ImageFormatError, +- 'Invalid VMDK descriptor', +- inspector.safety_check) +- +- # Extents with slashes in the name fail the safety check +- region.data = b'RW 123 SPARSE "/etc/shadow"' +- region.length = len(region.data) +- self.assertFalse(inspector.safety_check()) +- +- # A descriptor that specifies no extents fails the safety check +- region.data = b'# Nothing' +- region.length = len(region.data) +- self.assertFalse(inspector.safety_check()) +- +- def test_vmdk_reject_footer(self): +- data = struct.pack('<4sIIQQQQIQQ', b'KDMV', 3, 0, 0, 0, 0, 1, 0, 0, +- format_inspector.VMDKInspector.GD_AT_END) +- inspector = format_inspector.VMDKInspector() +- inspector.region('header').data = data +- inspector.region('header').length = len(data) +- self.assertRaisesRegex(format_inspector.ImageFormatError, +- 'footer', +- inspector.post_process) +- + def test_vdi(self): + self._test_format('vdi') + +-- +2.41.0 + + diff -Nru glance-25.1.0/debian/patches/CVE-2024-32498_3_7_glance-stable-2023.1.patch glance-25.1.0/debian/patches/CVE-2024-32498_3_7_glance-stable-2023.1.patch --- glance-25.1.0/debian/patches/CVE-2024-32498_3_7_glance-stable-2023.1.patch 1970-01-01 00:00:00.000000000 +0000 +++ glance-25.1.0/debian/patches/CVE-2024-32498_3_7_glance-stable-2023.1.patch 2024-06-21 08:38:56.000000000 +0000 @@ -0,0 +1,33 @@ +Description: CVE-2024-32498: [PATCH 7/7] Add safety check and detection support to FI tool + This adds a safety check and detection mechanism to the + tools/test_format_inspector.py utility for verifying those features + outside of glance. +Author: Dan Smith +Date: Wed, 26 Jun 2024 08:41:02 -0700 +Change-Id: I447e7e51315472f8fa6013d4c4852f54c1e0c43d +Bug: https://launchpad.net/bugs/2059809 +Bug-Debian: https://bugs.debian.org/1074761 +Origin: upstream, https://review.opendev.org/c/openstack/glance/+/923283 +Last-Update: 2024-06-30 + +diff --git a/tools/test_format_inspector.py b/tools/test_format_inspector.py +index aa554386e..63e23210c 100755 +--- a/tools/test_format_inspector.py ++++ b/tools/test_format_inspector.py +@@ -102,6 +102,13 @@ def main(): + else: + print('Confirmed size with qemu-img') + ++ print('Image safety check: %s' % ( ++ fmt.safety_check() and 'passed' or 'FAILED')) ++ if args.input: ++ detected_fmt = format_inspector.detect_file_format(args.input) ++ print('Detected inspector for image as: %s' % ( ++ detected_fmt.__class__.__name__)) ++ + + if __name__ == '__main__': + sys.exit(main()) +-- +2.41.0 + diff -Nru glance-25.1.0/debian/patches/series glance-25.1.0/debian/patches/series --- glance-25.1.0/debian/patches/series 2023-04-14 11:32:58.000000000 +0000 +++ glance-25.1.0/debian/patches/series 2024-06-21 08:38:56.000000000 +0000 @@ -1,2 +1,11 @@ sql_conn-registry.patch missing-files.patch +CVE-2024-32498_1_Limit_CaptureRegion_sizes_in_format_inspector_for_VMDK_and_VHDX.patch +CVE-2024-32498_2_Support_Stream_Optimized_VMDKs.patch +CVE-2024-32498_3_1_glance-stable-2023.1.patch +CVE-2024-32498_3_2_glance-stable-2023.1.patch +CVE-2024-32498_3_3_glance-stable-2023.1.patch +CVE-2024-32498_3_4_glance-stable-2023.1.patch +CVE-2024-32498_3_5_glance-stable-2023.1.patch +CVE-2024-32498_3_6_glance-stable-2023.1.patch +CVE-2024-32498_3_7_glance-stable-2023.1.patch