Skip to content

Commit 9fc07bf

Browse files
authored
[3.14] gh-161096: Reject invalid TZif abbreviation block sizes in zoneinfo (GH-161097) (#161123)
ZoneInfo.from_file() now raises ValueError for a negative charcnt or one above 512, and reads the abbreviation index as unsigned. ANT-2026-W7U7UDLT (cherry picked from commit e9f37a3)
1 parent e1484f1 commit 9fc07bf

3 files changed

Lines changed: 41 additions & 1 deletion

File tree

‎Lib/test/test_zoneinfo/test_zoneinfo.py‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -795,6 +795,34 @@ def test_invalid_transition_index(self):
795795
with self.assertRaises(ValueError):
796796
self.klass.from_file(f)
797797

798+
def test_invalid_charcnt(self):
799+
# 128 local time types whose abbreviations are the suffixes of a
800+
# single long abbreviation spanning the entire block: every index
801+
# would get its own copy of most of the block.
802+
types = b"".join(struct.pack(">lbb", 3600, 0, i) for i in range(128))
803+
chars = b"A" * 65535 + b"\x00"
804+
805+
# With a negative charcnt the block would be the rest of the file
806+
for charcnt in (len(chars), -1):
807+
with self.subTest(charcnt=charcnt):
808+
header = b"TZif" + b"\x00" * 16
809+
header += struct.pack(">6l", 0, 0, 0, 0, 128, charcnt)
810+
f = io.BytesIO(header + types + chars)
811+
812+
with self.assertRaises(ValueError):
813+
self.klass.from_file(f)
814+
815+
def test_abbreviation_index_above_127(self):
816+
chars = b"X" * 129 + b"\x00" + b"ABC\x00"
817+
header = b"TZif" + b"\x00" * 16
818+
header += struct.pack(">6l", 0, 0, 0, 0, 1, len(chars))
819+
types = struct.pack(">lbB", 3600, 0, 130)
820+
zi = self.klass.from_file(io.BytesIO(header + types + chars))
821+
822+
dt = datetime(2026, 1, 1, tzinfo=zi)
823+
self.assertEqual(dt.tzname(), "ABC")
824+
self.assertEqual(dt.utcoffset(), ONE_H)
825+
798826
def test_transition_lookahead_out_of_bounds(self):
799827
STD = ZoneOffset("STD", ZERO)
800828
DST = ZoneOffset("DST", ONE_H, ONE_H)

‎Lib/zoneinfo/_common.py‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ def load_data(fobj):
7878
# Read the ttinfo struct, (utoff, isdst, abbrind)
7979
if typecnt:
8080
utcoff, isdst, abbrind = zip(
81-
*(struct.unpack(">lbb", fobj.read(6)) for i in range(typecnt))
81+
*(struct.unpack(">lbB", fobj.read(6)) for i in range(typecnt))
8282
)
8383
else:
8484
utcoff = ()
@@ -89,6 +89,14 @@ def load_data(fobj):
8989
# not by position in the array but by position in the unsplit
9090
# abbreviation string. I suppose this makes more sense in C, which uses
9191
# null to terminate the strings, but it's inconvenient here...
92+
#
93+
# Each abbreviation index is a single unsigned byte, so only about the
94+
# first 256 characters of the block can be referenced. A much longer
95+
# block holds nothing usable, and reading it would copy most of the
96+
# block for every local time type.
97+
if not 0 <= charcnt <= 512:
98+
raise ValueError(f"Invalid TZif file: invalid charcnt: {charcnt}")
99+
92100
abbr_vals = {}
93101
abbr_chars = fobj.read(charcnt)
94102

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix :meth:`zoneinfo.ZoneInfo.from_file` accepting TZif data with a negative or
2+
oversized abbreviation block, which could use hundreds of times the file size
3+
in memory; it now raises :exc:`ValueError`. Abbreviation indexes above 127 now
4+
also give the correct time zone abbreviation.

0 commit comments

Comments
 (0)