Bug report
Bug description:
Summary
An Infinite Loop bug was identified in the pure-Python implementation of zoneinfo._common.load_data (used by _zoneinfo.c). The bug is triggered when parsing a ZoneInfo file (version >= 2) that ends unexpectedly while reading the timezone string. The loop condition while (c := fobj.read(1)) != b"\n" fails to handle the End-Of-File (EOF) condition, causing it to loop indefinitely if b"\n" is never found.
Component
Lib/zoneinfo/_common.py
Affected Functions
load_data
Root Cause Analysis
The code attempts to read the timezone string byte-by-byte until a newline character is found:
# Lib/zoneinfo/_common.py:122 while (c := fobj.read(1)) != b"\n": tz_bytes += c
If the file pointer reaches EOF, fobj.read(1) returns b"" (empty bytes).
The comparison b"" != b"\n" evaluates to True.
The loop continues indefinitely, appending empty bytes to tz_bytes (or doing nothing if tz_bytes is efficiently handled, but consuming CPU).
Reproduction
- Input: A constructed ZoneInfo file with Version 2 header but truncated before the trailing newline of the timezone string.
- Command:
python3 reproduce_issue_4.py
Script:
import io import zoneinfo import struct import time def reproduce(): # First Header (V1 part of V2 file) # Magic(4) + Version(1) + Reserved(15) + Counts(24) header = b"TZif" + b"2" + (b"\x00" * 15) + (b"\x00" * 24) # Data is empty (counts are 0) # Second Header (V2 itself) # Magic(4) + Version(1) + Reserved(15) + Counts(24) header2 = b"TZif" + b"2" + (b"\x00" * 15) + (b"\x00" * 24) # Newline check data = header + header2 + b"\n" print("This script will HANG INDEFINITELY if the vulnerability is present.") print("Press Ctrl+C to stop.") try: bio = io.BytesIO(data) print("Starting ZoneInfo.from_file...") try: zoneinfo.ZoneInfo.from_file(bio) except ValueError as e: if "unexpected end of file" in str(e): print("SUCCESS: Code raised ValueError as expected (Fix verified)") return else: raise e print("Finished without expected ValueError?") except KeyboardInterrupt: print("\nInterrupted by user.") except Exception as e: print(f"Caught exception: {e}") if __name__ == "__main__": reproduce()
Patch
I think this will solve the problem. I'll send a PR.
line = fobj.readline()
if not line.endswith(b"\n"):
raise ValueError("Invalid TZif file: unexpected end of file")
tz_bytes = line.rstrip(b"\n")
CPython versions tested on:
3.15
Operating systems tested on:
Linux