Skip to content

reject zones whose extended transitions are out of order - #365

Open
rajath201 wants to merge 2 commits into
google:masterfrom
rajath201:extend-transitions-order
Open

reject zones whose extended transitions are out of order#365
rajath201 wants to merge 2 commits into
google:masterfrom
rajath201:extend-transitions-order

Conversation

@rajath201

@rajath201 rajath201 commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

ExtendTransitions() generates the future transitions a year at a time and guards each generated pair with last_time, the unix time of the last transition loaded from the file. That bound never moves as transitions are pushed, so nothing keeps one year's pair ahead of the next year's. A transition time may carry a day offset of up to +/-167 hours, which is more slack than the day or so separating adjacent years, so a footer whose daylight time starts several days after its nominal date while the following year's ends several days before its own leaves transitions_ out of order in unix time. With STD-12:00:00DST12:00:00,358/100:00:00,1/-139:00:00 (ordinary +/-12h offsets) the same instant 1970-12-27T17:00:00Z resolves to offset 43200 or -18000 on the same time_zone, depending only on which instant was looked up before it.

No real zone loops civil times back on themselves this way, so reject such rules and fail the load: fold the unix-time ordering check into the loop that already checks the civil-time ordering of every transition, which covers the extended transitions as well as the loaded ones. Every zone in testdata/zoneinfo still loads.

// its own, so the two years' generated transitions overlap.
return std::unique_ptr<ZoneInfoSource>(new StringZoneInfoSource(
MakeExtendedTzif(0, -5 * 3600, std::string{"STD", 4},
"STD-12:00:00DST12:00:00,358/100:00:00,1/-139:00:00")));

@devbww devbww Aug 3, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Civil-to-absolute conversion in CCTZ is premised on the civil time of transitions being increasing. While this POSIX rule is expressible (with the extended zic rules), I would assert that it not practical given that it loops civil times back on themselves outside of a single, REPEATED transition. No real zone would ever behave that way.

So, much like other recent changes, I think the best tactic here is to reject such rules and cause the load_time_zone() to fail. That is, ...

--- a/src/time_zone_info.cc
+++ b/src/time_zone_info.cc
@@ -753,11 +753,6 @@ bool TimeZoneInfo::Load(ZoneInfoSource* zip) {
     if (transitions_[i].unix_time < -(1LL << 59) ||
         transitions_[i].unix_time > (1LL << 59))
       return false;  // out of range
-    if (i != 0) {
-      // Check that the transitions are ordered by time (as zic guarantees).
-      if (!Transition::ByUnixTime()(transitions_[i - 1], transitions_[i]))
-        return false;  // out of order
-    }
   }
   bool seen_type_0 = false;
   for (std::size_t i = 0; i != hdr.timecnt; ++i) {
@@ -880,11 +875,13 @@ bool TimeZoneInfo::Load(ZoneInfoSource* zip) {
     ttp = &transition_types_[tr.type_index];
     tr.civil_sec = LocalTime(tr.unix_time, *ttp).cs;
     if (i != 0) {
-      // Check that the transitions are ordered by civil time. Essentially
-      // this means that an offset change cannot cross another such change.
-      // No one does this in practice, and we depend on it in MakeTime().
-      if (!Transition::ByCivilTime()(transitions_[i - 1], tr))
+      // Check that offset changes don't cross each other. No one
+      // does this is practice, and we depend on increasing absolute
+      // and civil times in BreakTime() and MakeTime() respectively.
+      if (!Transition::ByUnixTime()(transitions_[i - 1], tr) ||
+          !Transition::ByCivilTime()(transitions_[i - 1], tr)) {
         return false;  // out of order
+      }
     }
   }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. Applied your diff (with the small "does this is practice" typo fixed) and flipped the test to expect load_time_zone() to fail. All four suites pass, and every zone in testdata/zoneinfo still loads since the consolidated check sees the same loaded transitions as before plus the extended ones.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me. Thanks. I'll now pass this along to @derekmauro.

One optional thing you could do in the meantime, if you like, is unify the ordering of the if (name == "test:Foo") statements with the TEST(TimeZoneEdgeCase, Foo) cases. They might as well be the same. There could even be some logical order.

A POSIX transition time may carry a day offset of up to +/-167 hours,
so the pair of transitions ExtendTransitions() generates for one year
can overlap the pair generated for the next, leaving transitions_ out
of order in unix time. BreakTime() binary searches that vector, so a
lookup on such a zone depends on what was looked up before it.

No real zone loops civil times back on themselves like this, so fold
the unix-time ordering check into the loop that already checks the
civil-time ordering of every transition, and fail the load instead.
@rajath201
rajath201 force-pushed the extend-transitions-order branch from 2936f3d to 772c6ca Compare August 4, 2026 18:12
@rajath201 rajath201 changed the title keep extended transitions ordered in ExtendTransitions reject zones whose extended transitions are out of order Aug 4, 2026
// its own, so the two years' generated transitions overlap.
return std::unique_ptr<ZoneInfoSource>(new StringZoneInfoSource(
MakeExtendedTzif(0, -5 * 3600, std::string{"STD", 4},
"STD-12:00:00DST12:00:00,358/100:00:00,1/-139:00:00")));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me. Thanks. I'll now pass this along to @derekmauro.

One optional thing you could do in the meantime, if you like, is unify the ordering of the if (name == "test:Foo") statements with the TEST(TimeZoneEdgeCase, Foo) cases. They might as well be the same. There could even be some logical order.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants