diff --git a/Lib/gettext.py b/Lib/gettext.py index 2f77f0e849e9ae..5f449d54c76fab 100644 --- a/Lib/gettext.py +++ b/Lib/gettext.py @@ -360,21 +360,31 @@ def _parse(self, fp): buf = fp.read() buflen = len(buf) # Are we big endian or little endian? + if buflen < 4: + raise OSError(0, 'File is corrupt', filename) magic = unpack('4I', buf[4:20]) ii = '>II' + hdr = '>4I' else: raise OSError(0, 'Bad magic number', filename) + if buflen < 20: + raise OSError(0, 'File is corrupt', filename) + version, msgcount, masteridx, transidx = unpack(hdr, buf[4:20]) major_version, minor_version = self._get_versions(version) if major_version not in self.VERSIONS: raise OSError(0, 'Bad version number ' + str(major_version), filename) + # The seek tables must lie within the file. + if (masteridx + 8 * msgcount > buflen + or transidx + 8 * msgcount > buflen): + raise OSError(0, 'File is corrupt', filename) + # Now put all messages from the .mo file buffer into the catalog # dictionary. for i in range(0, msgcount): diff --git a/Lib/test/test_gettext.py b/Lib/test/test_gettext.py index 9ad37909a8ec4e..126cfc6d394b84 100644 --- a/Lib/test/test_gettext.py +++ b/Lib/test/test_gettext.py @@ -1,6 +1,7 @@ import os import base64 import gettext +import io import unittest import unittest.mock from functools import partial @@ -314,6 +315,39 @@ def test_corrupt_file(self): self.assertEqual(exception.strerror, "File is corrupt") self.assertEqual(exception.filename, MOFILE_CORRUPT) + def test_truncated_header(self): + magic = b'\xde\x12\x04\x95' + for buf in (b'', magic, magic + bytes(15)): + with self.subTest(buflen=len(buf)): + with self.assertRaises(OSError) as cm: + gettext.GNUTranslations(io.BytesIO(buf)) + self.assertEqual(cm.exception.errno, 0) + self.assertEqual(cm.exception.strerror, "File is corrupt") + + def test_offsets_out_of_bounds(self): + buf = bytes([ + 0xde, 0x12, 0x04, 0x95, # Magic + 0x00, 0x00, 0x00, 0x00, # Version + 0x01, 0x00, 0x00, 0x00, # Message count + 0xe8, 0x03, 0x00, 0x00, # Message offset (past EOF) + 0xd0, 0x07, 0x00, 0x00, # Translation offset (past EOF) + ]) + with self.assertRaises(OSError) as cm: + gettext.GNUTranslations(io.BytesIO(buf)) + self.assertEqual(cm.exception.errno, 0) + self.assertEqual(cm.exception.strerror, "File is corrupt") + + def test_empty_catalog(self): + buf = bytes([ + 0xde, 0x12, 0x04, 0x95, # Magic + 0x00, 0x00, 0x00, 0x00, # Version + 0x00, 0x00, 0x00, 0x00, # Message count + 0x14, 0x00, 0x00, 0x00, # Message offset + 0x14, 0x00, 0x00, 0x00, # Translation offset + ]) + t = gettext.GNUTranslations(io.BytesIO(buf)) + self.assertEqual(t.gettext('foo'), 'foo') + def test_big_endian_file(self): with open(MOFILE_BIG_ENDIAN, 'rb') as fp: t = gettext.GNUTranslations(fp) diff --git a/Misc/NEWS.d/next/Library/2026-07-11-16-00-00.gh-issue-153605.Rk7Xz3.rst b/Misc/NEWS.d/next/Library/2026-07-11-16-00-00.gh-issue-153605.Rk7Xz3.rst new file mode 100644 index 00000000000000..a08fde3045c077 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-11-16-00-00.gh-issue-153605.Rk7Xz3.rst @@ -0,0 +1,2 @@ +Fix :class:`gettext.GNUTranslations` raising :exc:`struct.error` instead of +:exc:`OSError` when reading a malformed ``.mo`` file. Patch by tonghuaroot.