https://gcc.gnu.org/bugzilla/show_bug.cgi?id=127815

            Bug ID: 127815
           Summary: std::chrono::time_zone::get_info throws for an instant
                    before year::min() when the zone's first period
                    follows a rule set
           Product: gcc
           Version: 15.3.0
            Status: UNCONFIRMED
          Severity: normal
          Priority: P3
         Component: libstdc++
          Assignee: unassigned at gcc dot gnu.org
          Reporter: liweifriends at gmail dot com
  Target Milestone: ---

Since tzdata 2026d, CST6CDT, EST5EDT, MST7MDT and PST8PDT are no longer links
to America/Chicago and co. but Zones whose only line follows the US rules from
the beginning of time:

  Z CST6CDT -6 US C%sT

On such a zone, get_info(sys_seconds::min()) throws
std::length_error("vector::reserve"), and an instant less than a year before
January 1 of year::min() throws std::runtime_error("time_zone::get_info").
A zone whose first period has no rules ("-") answers the same queries
normally (America/Chicago returns its LMT period).

time_zone::_M_get_sys_info starts a zone's first period at January 1 of
year::min(). When tp is earlier than that and the period follows a rule set,
the year count from the period start to tp is negative; it is stored in an
int (truncating the int64 count: -292276991853 becomes -219215725 for
sys_seconds::min()), and std::min(100, n * 2) is passed to reserve(), which
converts the negative value to a huge size_t. When the count rounds to 0,
reserve() is skipped, but the expansion never yields a period containing tp,
so the function throws at the end.

libstdc++-v3/src/c++20/tzdb.cc, master at 080bfd8da2e (2026-10-07):
lines 1088, 1109-1110, 1251; releases/gcc-15 at a3b110420ad: lines 816,
837-838, 1033. The releases/gcc-16 branch has the same code (lines
1098-1099 at 7866d2f8e06).

The standard puts no lower bound on get_info, and zones without rules already
answer such queries, so a rule-based first period could do the same, e.g. by
clamping tp to the period's start before expanding.

The reproducer below does not depend on the system tzdata. Note that get_info
caches the expanded periods in the zone, so querying a later instant first
makes a following min() + 1s query appear to succeed; the reproducer queries
the failing instants first.

Reproduced with GCC 15.3.0 (Debian 13, gcc:15 Docker image) and GCC 15.3.1
(Fedora 43):

$ g++ -std=c++20 repro.cpp && ./a.out && ./a.out plain
Test: threw vector::reserve
Test: threw time_zone::get_info
Plain: ok, abbrev=CST
Plain: ok, abbrev=CST

----- repro.cpp -----

#include <chrono>
#include <cstdio>
#include <filesystem>
#include <fstream>

namespace __gnu_cxx
{
  // libstdc++ hook: read tzdata.zi from this directory instead of the
system's.
  const char* zoneinfo_dir_override() { return "/tmp/pr-tzdb-repro"; }
}

int main(int argc, char**)
{
  std::filesystem::create_directories("/tmp/pr-tzdb-repro");
  std::ofstream("/tmp/pr-tzdb-repro/tzdata.zi")
    << "# version test\n"
       "R X 2000 ma - Mar lastSu 2 1 D\n"
       "R X 2000 ma - O lastSu 2 0 S\n"
       "Z Test -6 X C%sT\n"
       "Z Plain -6 - CST\n";

  using namespace std::chrono;
  // One zone per run, so no earlier call has expanded (and cached) it.
  const char* name = argc > 1 ? "Plain" : "Test";
  const sys_seconds ymin{sys_days{year::min() / January / 1}};
  for (sys_seconds t : {sys_seconds::min(), ymin - 1s})
    try
      {
        sys_info i = locate_zone(name)->get_info(t);
        std::printf("%s: ok, abbrev=%s\n", name, i.abbrev.c_str());
      }
    catch (const std::exception& e)
      {
        std::printf("%s: threw %s\n", name, e.what());
      }
}

Reply via email to