diff --git a/.gitattributes b/.gitattributes index 0dac0f84927119..ffe5ac029d53c5 100644 --- a/.gitattributes +++ b/.gitattributes @@ -19,6 +19,7 @@ *.zip binary # Specific binary files +Lib/test/test_zoneinfo/data/tzif_* binary PC/classicAppCompat.* binary # Text files that should not be subject to eol conversion diff --git a/Lib/test/test_zoneinfo/data/tzif_invalid_lookahead b/Lib/test/test_zoneinfo/data/tzif_invalid_lookahead new file mode 100644 index 00000000000000..a1a4327ab93af7 Binary files /dev/null and b/Lib/test/test_zoneinfo/data/tzif_invalid_lookahead differ diff --git a/Lib/test/test_zoneinfo/data/tzif_invalid_trans_idx b/Lib/test/test_zoneinfo/data/tzif_invalid_trans_idx new file mode 100644 index 00000000000000..b438f996727780 Binary files /dev/null and b/Lib/test/test_zoneinfo/data/tzif_invalid_trans_idx differ diff --git a/Lib/test/test_zoneinfo/test_zoneinfo.py b/Lib/test/test_zoneinfo/test_zoneinfo.py index a5dea802a9898d..bb8ba5fef8be61 100644 --- a/Lib/test/test_zoneinfo/test_zoneinfo.py +++ b/Lib/test/test_zoneinfo/test_zoneinfo.py @@ -741,6 +741,16 @@ def test_empty_zone(self): with self.assertRaises(ValueError): self.klass.from_file(zf) + def test_invalid_transition_index(self): + with open(DATA_DIR / "tzif_invalid_trans_idx", "rb") as f: + with self.assertRaises(ValueError): + self.klass.from_file(f) + + def test_transition_lookahead_out_of_bounds(self): + with open(DATA_DIR / "tzif_invalid_lookahead", "rb") as f: + zi = self.klass.from_file(f) + self.assertIsNotNone(zi) + def test_zone_very_large_timestamp(self): """Test when a transition is in the far past or future. diff --git a/Lib/zoneinfo/_common.py b/Lib/zoneinfo/_common.py index 59f3f0ce853f74..c12b23b6c83472 100644 --- a/Lib/zoneinfo/_common.py +++ b/Lib/zoneinfo/_common.py @@ -71,6 +71,10 @@ def load_data(fobj): trans_list_utc = () trans_idx = () + for idx in trans_idx: + if idx >= typecnt: + raise ValueError(f"Invalid transition index found while reading TZif: {idx}") + # Read the ttinfo struct, (utoff, isdst, abbrind) if typecnt: utcoff, isdst, abbrind = zip( diff --git a/Lib/zoneinfo/_zoneinfo.py b/Lib/zoneinfo/_zoneinfo.py index bd3fefc6c9d959..7063eb6a9025ac 100644 --- a/Lib/zoneinfo/_zoneinfo.py +++ b/Lib/zoneinfo/_zoneinfo.py @@ -338,7 +338,7 @@ def _utcoff_to_dstoff(trans_idx, utcoffsets, isdsts): if not isdsts[comp_idx]: dstoff = utcoff - utcoffsets[comp_idx] - if not dstoff and idx < (typecnt - 1): + if not dstoff and idx < (typecnt - 1) and i + 1 < len(trans_idx): comp_idx = trans_idx[i + 1] # If the following transition is also DST and we couldn't diff --git a/Misc/NEWS.d/next/Library/2026-03-12-21-01-48.gh-issue-145883.lUvXcc.rst b/Misc/NEWS.d/next/Library/2026-03-12-21-01-48.gh-issue-145883.lUvXcc.rst new file mode 100644 index 00000000000000..2c17768c5189da --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-03-12-21-01-48.gh-issue-145883.lUvXcc.rst @@ -0,0 +1,2 @@ +:mod:`zoneinfo`: Fix heap buffer overflow reads from malformed TZif data. +Found by OSS Fuzz, issues :oss-fuzz:`492245058` and :oss-fuzz:`492230068`. diff --git a/Modules/_zoneinfo.c b/Modules/_zoneinfo.c index 39671d1ab51dfa..bd151101723cd7 100644 --- a/Modules/_zoneinfo.c +++ b/Modules/_zoneinfo.c @@ -1065,7 +1065,7 @@ load_data(zoneinfo_state *state, PyZoneInfo_ZoneInfo *self, PyObject *file_obj) } trans_idx[i] = (size_t)cur_trans_idx; - if (trans_idx[i] > self->num_ttinfos) { + if (trans_idx[i] >= self->num_ttinfos) { PyErr_Format( PyExc_ValueError, "Invalid transition index found while reading TZif: %zd", @@ -2063,7 +2063,7 @@ utcoff_to_dstoff(size_t *trans_idx, long *utcoffs, long *dstoffs, dstoff = utcoff - utcoffs[comp_idx]; } - if (!dstoff && idx < (num_ttinfos - 1)) { + if (!dstoff && idx < (num_ttinfos - 1) && i + 1 < num_transitions) { comp_idx = trans_idx[i + 1]; // If the following transition is also DST and we couldn't find diff --git a/Tools/build/compute-changes.py b/Tools/build/compute-changes.py index 4d92b083026b27..7f2b46cefcc20b 100644 --- a/Tools/build/compute-changes.py +++ b/Tools/build/compute-changes.py @@ -98,6 +98,9 @@ Path("Modules/pyexpat.c"), # zipfile Path("Lib/zipfile/"), + # zoneinfo + Path("Lib/zoneinfo/"), + Path("Modules/_zoneinfo.c"), })