diff --git a/Doc/library/zoneinfo.rst b/Doc/library/zoneinfo.rst index 8147e58d322667..759ec4277b8b7d 100644 --- a/Doc/library/zoneinfo.rst +++ b/Doc/library/zoneinfo.rst @@ -206,6 +206,9 @@ The ``ZoneInfo`` class has two alternate constructors: Objects created via this constructor cannot be pickled (see `pickling`_). + :exc:`ValueError` is raised if the data read from *file_obj* is not a valid + TZif file. + .. classmethod:: ZoneInfo.no_cache(key) An alternate constructor that bypasses the constructor's cache. It is diff --git a/Lib/test/test_zoneinfo/test_zoneinfo.py b/Lib/test/test_zoneinfo/test_zoneinfo.py index 8f3ca59c9ef5ed..581072d0701d65 100644 --- a/Lib/test/test_zoneinfo/test_zoneinfo.py +++ b/Lib/test/test_zoneinfo/test_zoneinfo.py @@ -252,6 +252,8 @@ def test_bad_zones(self): bad_zones = [ b"", # Empty file b"AAAA3" + b" " * 15, # Bad magic + # Truncated V2 file (should not loop indefinitely) + b"TZif2" + (b"\x00" * 39) + b"TZif2" + (b"\x00" * 39) + b"\n" + b"Part", ] for bad_zone in bad_zones: diff --git a/Lib/zoneinfo/_common.py b/Lib/zoneinfo/_common.py index 03cc42149f9b74..59f3f0ce853f74 100644 --- a/Lib/zoneinfo/_common.py +++ b/Lib/zoneinfo/_common.py @@ -118,11 +118,10 @@ def get_abbr(idx): c = fobj.read(1) # Should be \n assert c == b"\n", c - tz_bytes = b"" - while (c := fobj.read(1)) != b"\n": - tz_bytes += c - - tz_str = tz_bytes + line = fobj.readline() + if not line.endswith(b"\n"): + raise ValueError("Invalid TZif file: unexpected end of file") + tz_str = line.rstrip(b"\n") else: tz_str = None diff --git a/Misc/NEWS.d/next/Library/2025-12-28-13-49-06.gh-issue-143241.5H4b8d.rst b/Misc/NEWS.d/next/Library/2025-12-28-13-49-06.gh-issue-143241.5H4b8d.rst new file mode 100644 index 00000000000000..7170a06015ee7c --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-12-28-13-49-06.gh-issue-143241.5H4b8d.rst @@ -0,0 +1,2 @@ +:mod:`zoneinfo`: fix infinite loop in :meth:`ZoneInfo.from_file +` when parsing a malformed TZif file. Patch by Fatih Celik.