diff --git a/Lib/gettext.py b/Lib/gettext.py index f681f69b9827cbf..91ae3bdc82465ff 100644 --- a/Lib/gettext.py +++ b/Lib/gettext.py @@ -362,6 +362,8 @@ def _parse(self, fp): self.plural = lambda n: int(n != 1) # germanic plural by default buf = fp.read() buflen = len(buf) + if buflen < 20: + raise OSError(0, 'File is corrupt', filename) # Are we big endian or little endian? magic = unpack(' 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 b18489f34ae4806..a1830d4c39c08e4 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 @@ -39,7 +40,7 @@ ''' # .mo file with an invalid magic number -GNU_MO_DATA_BAD_MAGIC_NUMBER = base64.b64encode(b'ABCD') +GNU_MO_DATA_BAD_MAGIC_NUMBER = base64.b64encode(b'ABCD' + bytes(16)) # This data contains an invalid major version number (5) # An unexpected major version number should be treated as an error when @@ -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 000000000000000..a08fde3045c0778 --- /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.