Skip to content

Commit dabdd4f

Browse files
committed
gh-153605: Fix gettext.GNUTranslations struct.error on a malformed .mo file
GNUTranslations._parse unpacked the .mo header and seek tables without checking the file was long enough, so a truncated file or one whose seek tables point past the end leaked a struct.error instead of the OSError the module raises for other malformed input. Validate the lengths first.
1 parent c22e9c9 commit dabdd4f

3 files changed

Lines changed: 48 additions & 2 deletions

File tree

Lib/gettext.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -360,21 +360,31 @@ def _parse(self, fp):
360360
buf = fp.read()
361361
buflen = len(buf)
362362
# Are we big endian or little endian?
363+
if buflen < 4:
364+
raise OSError(0, 'File is corrupt', filename)
363365
magic = unpack('<I', buf[:4])[0]
364366
if magic == self.LE_MAGIC:
365-
version, msgcount, masteridx, transidx = unpack('<4I', buf[4:20])
366367
ii = '<II'
368+
hdr = '<4I'
367369
elif magic == self.BE_MAGIC:
368-
version, msgcount, masteridx, transidx = unpack('>4I', buf[4:20])
369370
ii = '>II'
371+
hdr = '>4I'
370372
else:
371373
raise OSError(0, 'Bad magic number', filename)
374+
if buflen < 20:
375+
raise OSError(0, 'File is corrupt', filename)
376+
version, msgcount, masteridx, transidx = unpack(hdr, buf[4:20])
372377

373378
major_version, minor_version = self._get_versions(version)
374379

375380
if major_version not in self.VERSIONS:
376381
raise OSError(0, 'Bad version number ' + str(major_version), filename)
377382

383+
# The seek tables must lie within the file.
384+
if (masteridx + 8 * msgcount > buflen
385+
or transidx + 8 * msgcount > buflen):
386+
raise OSError(0, 'File is corrupt', filename)
387+
378388
# Now put all messages from the .mo file buffer into the catalog
379389
# dictionary.
380390
for i in range(0, msgcount):

Lib/test/test_gettext.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import os
22
import base64
33
import gettext
4+
import io
45
import unittest
56
import unittest.mock
67
from functools import partial
@@ -314,6 +315,39 @@ def test_corrupt_file(self):
314315
self.assertEqual(exception.strerror, "File is corrupt")
315316
self.assertEqual(exception.filename, MOFILE_CORRUPT)
316317

318+
def test_truncated_header(self):
319+
magic = b'\xde\x12\x04\x95'
320+
for buf in (b'', magic, magic + bytes(15)):
321+
with self.subTest(buflen=len(buf)):
322+
with self.assertRaises(OSError) as cm:
323+
gettext.GNUTranslations(io.BytesIO(buf))
324+
self.assertEqual(cm.exception.errno, 0)
325+
self.assertEqual(cm.exception.strerror, "File is corrupt")
326+
327+
def test_offsets_out_of_bounds(self):
328+
buf = bytes([
329+
0xde, 0x12, 0x04, 0x95, # Magic
330+
0x00, 0x00, 0x00, 0x00, # Version
331+
0x01, 0x00, 0x00, 0x00, # Message count
332+
0xe8, 0x03, 0x00, 0x00, # Message offset (past EOF)
333+
0xd0, 0x07, 0x00, 0x00, # Translation offset (past EOF)
334+
])
335+
with self.assertRaises(OSError) as cm:
336+
gettext.GNUTranslations(io.BytesIO(buf))
337+
self.assertEqual(cm.exception.errno, 0)
338+
self.assertEqual(cm.exception.strerror, "File is corrupt")
339+
340+
def test_empty_catalog(self):
341+
buf = bytes([
342+
0xde, 0x12, 0x04, 0x95, # Magic
343+
0x00, 0x00, 0x00, 0x00, # Version
344+
0x00, 0x00, 0x00, 0x00, # Message count
345+
0x14, 0x00, 0x00, 0x00, # Message offset
346+
0x14, 0x00, 0x00, 0x00, # Translation offset
347+
])
348+
t = gettext.GNUTranslations(io.BytesIO(buf))
349+
self.assertEqual(t.gettext('foo'), 'foo')
350+
317351
def test_big_endian_file(self):
318352
with open(MOFILE_BIG_ENDIAN, 'rb') as fp:
319353
t = gettext.GNUTranslations(fp)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix :class:`gettext.GNUTranslations` raising :exc:`struct.error` instead of
2+
:exc:`OSError` when reading a malformed ``.mo`` file. Patch by tonghuaroot.

0 commit comments

Comments
 (0)