https://github.com/python/cpython/commit/9fc07bfcbedbd8f9cb2e1aee6fa37f9a2c4a9615
commit: 9fc07bfcbedbd8f9cb2e1aee6fa37f9a2c4a9615
branch: 3.14
author: Gregory P. Smith <[email protected]>
committer: gpshead <[email protected]>
date: 2026-11-01T19:22:31Z
summary:

[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 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 6f87cc91af04551..128224c0d929c3e 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]

Reply via email to