https://github.com/python/cpython/commit/8b66c4c580bcaa5202f6d412ead6b6aa954e9a4b
commit: 8b66c4c580bcaa5202f6d412ead6b6aa954e9a4b
branch: 3.15
author: Gregory P. Smith <[email protected]>
committer: gpshead <[email protected]>
date: 2026-11-01T19:27:38Z
summary:
[3.15] gh-161096: Reject invalid TZif abbreviation block sizes in zoneinfo
(GH-161097) (#161122)
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 e9f37a3663b8ab83910e5ea0fb8b18b08607fab3)
files:
A Misc/NEWS.d/next/Library/2026-10-22-05-20-52.gh-issue-161096.vkhfyy.rst
M Lib/test/test_zoneinfo/test_zoneinfo.py
M Lib/zoneinfo/_common.py
diff --git a/Lib/test/test_zoneinfo/test_zoneinfo.py
b/Lib/test/test_zoneinfo/test_zoneinfo.py
index 05ceb865cffc3f5..d4b3cd36e6d3293 100644
--- a/Lib/test/test_zoneinfo/test_zoneinfo.py
+++ b/Lib/test/test_zoneinfo/test_zoneinfo.py
@@ -795,6 +795,34 @@ def test_invalid_transition_index(self):
with self.assertRaises(ValueError):
self.klass.from_file(f)
+ def test_invalid_charcnt(self):
+ # 128 local time types whose abbreviations are the suffixes of a
+ # single long abbreviation spanning the entire block: every index
+ # would get its own copy of most of the block.
+ types = b"".join(struct.pack(">lbb", 3600, 0, i) for i in range(128))
+ chars = b"A" * 65535 + b"\x00"
+
+ # With a negative charcnt the block would be the rest of the file
+ for charcnt in (len(chars), -1):
+ with self.subTest(charcnt=charcnt):
+ header = b"TZif" + b"\x00" * 16
+ header += struct.pack(">6l", 0, 0, 0, 0, 128, charcnt)
+ f = io.BytesIO(header + types + chars)
+
+ with self.assertRaises(ValueError):
+ self.klass.from_file(f)
+
+ def test_abbreviation_index_above_127(self):
+ chars = b"X" * 129 + b"\x00" + b"ABC\x00"
+ header = b"TZif" + b"\x00" * 16
+ header += struct.pack(">6l", 0, 0, 0, 0, 1, len(chars))
+ types = struct.pack(">lbB", 3600, 0, 130)
+ zi = self.klass.from_file(io.BytesIO(header + types + chars))
+
+ dt = datetime(2026, 1, 1, tzinfo=zi)
+ self.assertEqual(dt.tzname(), "ABC")
+ self.assertEqual(dt.utcoffset(), ONE_H)
+
def test_transition_lookahead_out_of_bounds(self):
STD = ZoneOffset("STD", ZERO)
DST = ZoneOffset("DST", ONE_H, ONE_H)
diff --git a/Lib/zoneinfo/_common.py b/Lib/zoneinfo/_common.py
index 98668c15d8bf94b..d761fdce2c7a677 100644
--- a/Lib/zoneinfo/_common.py
+++ b/Lib/zoneinfo/_common.py
@@ -78,7 +78,7 @@ def load_data(fobj):
# Read the ttinfo struct, (utoff, isdst, abbrind)
if typecnt:
utcoff, isdst, abbrind = zip(
- *(struct.unpack(">lbb", fobj.read(6)) for i in range(typecnt))
+ *(struct.unpack(">lbB", fobj.read(6)) for i in range(typecnt))
)
else:
utcoff = ()
@@ -89,6 +89,14 @@ def load_data(fobj):
# not by position in the array but by position in the unsplit
# abbreviation string. I suppose this makes more sense in C, which uses
# null to terminate the strings, but it's inconvenient here...
+ #
+ # Each abbreviation index is a single unsigned byte, so only about the
+ # first 256 characters of the block can be referenced. A much longer
+ # block holds nothing usable, and reading it would copy most of the
+ # block for every local time type.
+ if not 0 <= charcnt <= 512:
+ raise ValueError(f"Invalid TZif file: invalid charcnt: {charcnt}")
+
abbr_vals = {}
abbr_chars = fobj.read(charcnt)
diff --git
a/Misc/NEWS.d/next/Library/2026-10-22-05-20-52.gh-issue-161096.vkhfyy.rst
b/Misc/NEWS.d/next/Library/2026-10-22-05-20-52.gh-issue-161096.vkhfyy.rst
new file mode 100644
index 000000000000000..bae5465f42c6a18
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-10-22-05-20-52.gh-issue-161096.vkhfyy.rst
@@ -0,0 +1,4 @@
+Fix :meth:`zoneinfo.ZoneInfo.from_file` accepting TZif data with a negative or
+oversized abbreviation block, which could use hundreds of times the file size
+in memory; it now raises :exc:`ValueError`. Abbreviation indexes above 127 now
+also give the correct time zone abbreviation.
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]