Import of CCTZ from GitHub.

PiperOrigin-RevId: 958524522
Change-Id: I64ef0e82ebf7996c08498d065920cd8216b7f3ad
diff --git a/absl/time/internal/cctz/BUILD.bazel b/absl/time/internal/cctz/BUILD.bazel
index 3b47877..edec282 100644
--- a/absl/time/internal/cctz/BUILD.bazel
+++ b/absl/time/internal/cctz/BUILD.bazel
@@ -140,7 +140,10 @@
     name = "time_zone_lookup_test",
     size = "small",
     timeout = "moderate",
-    srcs = ["src/time_zone_lookup_test.cc"],
+    srcs = [
+        "src/time_zone_lookup_test.cc",
+        "src/tzfile.h",
+    ],
     copts = ABSL_TEST_COPTS,
     data = [":zoneinfo"],
     linkopts = ABSL_DEFAULT_LINKOPTS,
@@ -177,6 +180,24 @@
     ],
 )
 
+cc_test(
+    name = "time_zone_posix_test",
+    size = "small",
+    srcs = [
+        "src/time_zone_posix.h",
+        "src/time_zone_posix_test.cc",
+    ],
+    copts = ABSL_TEST_COPTS,
+    linkopts = ABSL_DEFAULT_LINKOPTS,
+    deps = [
+        ":civil_time",
+        ":time_zone",
+        "//absl/base:config",
+        "@googletest//:gtest",
+        "@googletest//:gtest_main",
+    ],
+)
+
 ### benchmarks
 
 cc_test(
diff --git a/absl/time/internal/cctz/src/cctz_benchmark.cc b/absl/time/internal/cctz/src/cctz_benchmark.cc
index ce27818..17ff576 100644
--- a/absl/time/internal/cctz/src/cctz_benchmark.cc
+++ b/absl/time/internal/cctz/src/cctz_benchmark.cc
@@ -28,6 +28,7 @@
 
 namespace {
 
+// SKIP_ABSL_INLINE_NAMESPACE_CHECK
 namespace cctz = absl::time_internal::cctz;
 
 void BM_Difference_Days(benchmark::State& state) {
diff --git a/absl/time/internal/cctz/src/time_zone_fixed.cc b/absl/time/internal/cctz/src/time_zone_fixed.cc
index ed7f9cb..b33410d 100644
--- a/absl/time/internal/cctz/src/time_zone_fixed.cc
+++ b/absl/time/internal/cctz/src/time_zone_fixed.cc
@@ -40,12 +40,19 @@
   return p;
 }
 
+// Returns the value of the decimal digit ch, or -1 if ch is not a digit.
+// Note that std::strchr() also matches kDigits' terminating '\0', which
+// would otherwise be taken for a tenth digit.
+int ParseDigit(char ch) {
+  const char* const dp = std::strchr(kDigits, ch);
+  return (dp == nullptr || *dp == '\0') ? -1 : static_cast<int>(dp - kDigits);
+}
+
 int Parse02d(const char* p) {
-  if (const char* ap = std::strchr(kDigits, *p)) {
-    int v = static_cast<int>(ap - kDigits);
-    if (const char* bp = std::strchr(kDigits, *++p)) {
-      return (v * 10) + static_cast<int>(bp - kDigits);
-    }
+  const int hi = ParseDigit(p[0]);
+  if (hi >= 0) {
+    const int lo = ParseDigit(p[1]);
+    if (lo >= 0) return (hi * 10) + lo;
   }
   return -1;
 }
diff --git a/absl/time/internal/cctz/src/time_zone_format.cc b/absl/time/internal/cctz/src/time_zone_format.cc
index 91b4621..f97de2f 100644
--- a/absl/time/internal/cctz/src/time_zone_format.cc
+++ b/absl/time/internal/cctz/src/time_zone_format.cc
@@ -58,6 +58,16 @@
 
 namespace {
 
+// The ctype functions have undefined behavior for negative char values,
+// so these helpers ensure the argument is always in the unsigned-char domain.
+bool isdigit(char ch) {
+  return std::isdigit(static_cast<unsigned char>(ch)) != 0;
+}
+
+bool isspace(char ch) {
+  return std::isspace(static_cast<unsigned char>(ch)) != 0;
+}
+
 #if !HAS_STRPTIME
 // Build a strptime() using C++11's std::get_time().
 char* strptime(const char* s, const char* fmt, std::tm* tm) {
@@ -553,7 +563,7 @@
       bp = Format64(ep, 4, al.cs.year());
       result.append(bp, ep);
       pending = cur += 2;
-    } else if (std::isdigit(*cur)) {
+    } else if (isdigit(*cur)) {
       // Possibly found %E#S or %E#f.
       int n = 0;
       if (const char* np = ParseInt(cur, 0, 0, 1024, &n)) {
@@ -620,7 +630,7 @@
 const char* ParseZone(const char* dp, std::string* zone) {
   zone->clear();
   if (dp != nullptr) {
-    while (*dp != '\0' && !std::isspace(*dp)) zone->push_back(*dp++);
+    while (*dp != '\0' && !isspace(*dp)) zone->push_back(*dp++);
     if (zone->empty()) dp = nullptr;
   }
   return dp;
@@ -706,7 +716,7 @@
   const char* const edata = data + input.size();
 
   // Skips leading whitespace.
-  while (std::isspace(*data)) ++data;
+  while (isspace(*data)) ++data;
 
   const year_t kyearmax = std::numeric_limits<year_t>::max();
   const year_t kyearmin = std::numeric_limits<year_t>::min();
@@ -744,9 +754,9 @@
 
   // Steps through format, one specifier at a time.
   while (data != nullptr && fmt != efmt) {
-    if (std::isspace(*fmt)) {
-      while (std::isspace(*data)) ++data;
-      while (std::isspace(*++fmt)) continue;
+    if (isspace(*fmt)) {
+      while (isspace(*data)) ++data;
+      while (isspace(*++fmt)) continue;
       continue;
     }
 
@@ -898,7 +908,7 @@
           continue;
         }
         if (fmt[0] == '*' && fmt[1] == 'f') {
-          if (data != nullptr && std::isdigit(*data)) {
+          if (data != nullptr && isdigit(*data)) {
             data = ParseSubSeconds(data, &subseconds);
           }
           fmt += 2;
@@ -917,7 +927,7 @@
           fmt += 2;
           continue;
         }
-        if (std::isdigit(*fmt)) {
+        if (isdigit(*fmt)) {
           int n = 0;  // value ignored
           if (const char* np = ParseInt(fmt, 0, 0, 1024, &n)) {
             if (*np == 'S') {
@@ -929,7 +939,7 @@
               continue;
             }
             if (*np == 'f') {
-              if (data != nullptr && std::isdigit(*data)) {
+              if (data != nullptr && isdigit(*data)) {
                 data = ParseSubSeconds(data, &subseconds);
               }
               fmt = ++np;
@@ -977,7 +987,7 @@
   }
 
   // Skip any remaining whitespace.
-  while (std::isspace(*data)) ++data;
+  while (isspace(*data)) ++data;
 
   // parse() must consume the entire input string.
   if (data != edata) {
diff --git a/absl/time/internal/cctz/src/time_zone_format_test.cc b/absl/time/internal/cctz/src/time_zone_format_test.cc
index f047d93..8793a80 100644
--- a/absl/time/internal/cctz/src/time_zone_format_test.cc
+++ b/absl/time/internal/cctz/src/time_zone_format_test.cc
@@ -1002,6 +1002,27 @@
   EXPECT_FALSE(parse("%Ez", "-00:-0", tz, &tp));
 }
 
+TEST(Parse, NonAsciiInput) {
+  const time_zone tz = utc_time_zone();
+  auto tp = chrono::system_clock::from_time_t(0);
+
+  // High-bit-set bytes reach the ctype functions during parsing. 0xA0 is not
+  // ASCII whitespace, so the leading-whitespace skip must not consume it and
+  // the parse must fail rather than pass a negative char to std::isspace().
+  EXPECT_FALSE(parse("%Y-%m-%d",
+                     "\xA0"
+                     "2016-01-02",
+                     tz, &tp));
+  EXPECT_FALSE(parse("%Y",
+                     "\xA0"
+                     "2016",
+                     tz, &tp));
+
+  // A leading ASCII space is still skipped as before.
+  EXPECT_TRUE(parse("%Y-%m-%d", " 2016-01-02", tz, &tp));
+  EXPECT_EQ(2016, convert(tp, utc_time_zone()).year());
+}
+
 TEST(Parse, PosixConversions) {
   time_zone tz = utc_time_zone();
   auto tp = chrono::system_clock::from_time_t(0);
diff --git a/absl/time/internal/cctz/src/time_zone_impl.cc b/absl/time/internal/cctz/src/time_zone_impl.cc
index 5f2f49e..5cf38e7 100644
--- a/absl/time/internal/cctz/src/time_zone_impl.cc
+++ b/absl/time/internal/cctz/src/time_zone_impl.cc
@@ -76,9 +76,18 @@
   // Add the new time zone to the map.
   std::lock_guard<std::mutex> lock(TimeZoneMutex());
   if (time_zone_map == nullptr) time_zone_map = new TimeZoneImplByName;
+  if (!new_impl->zone_) {
+    // Load failed, but a successful insertion may have happened concurrently.
+    // Check it now that we have the lock. Otherwise, avoid caching negative
+    // entries to avoid unbounded growth and DoS attacks.
+    auto itr = time_zone_map->find(name);
+    const Impl* impl = (itr != time_zone_map->end()) ? itr->second : utc_impl;
+    *tz = time_zone(impl);
+    return impl != utc_impl;
+  }
   const Impl*& impl = (*time_zone_map)[name];
   if (impl == nullptr) {  // this thread won any load race
-    impl = new_impl->zone_ ? new_impl.release() : utc_impl;
+    impl = new_impl.release();
   }
   *tz = time_zone(impl);
   return impl != utc_impl;
diff --git a/absl/time/internal/cctz/src/time_zone_info.cc b/absl/time/internal/cctz/src/time_zone_info.cc
index f8484c9..1e707c8 100644
--- a/absl/time/internal/cctz/src/time_zone_info.cc
+++ b/absl/time/internal/cctz/src/time_zone_info.cc
@@ -32,6 +32,14 @@
 
 #include "absl/time/internal/cctz/src/time_zone_info.h"
 
+#include "absl/base/config.h"
+
+#if !defined(_MSC_VER)
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#endif
+
 #include <algorithm>
 #include <cassert>
 #include <chrono>
@@ -41,16 +49,17 @@
 #include <cstring>
 #include <fstream>
 #include <functional>
+#include <limits>
 #include <memory>
 #include <sstream>
 #include <string>
 #include <utility>
 #include <vector>
 
-#include "absl/base/config.h"
 #include "absl/time/internal/cctz/include/cctz/civil_time.h"
 #include "absl/time/internal/cctz/src/time_zone_fixed.h"
 #include "absl/time/internal/cctz/src/time_zone_posix.h"
+#include "absl/time/internal/cctz/src/tzfile.h"
 
 namespace absl {
 ABSL_NAMESPACE_BEGIN
@@ -338,6 +347,14 @@
     return EquivTransitions(transitions_.back().type_index, dst_ti);
   }
 
+  // We require that zoneinfo data with a rule for future transitions
+  // ends with a non-negative transition.  This removes the need to add
+  // any "second-half" transition to ensure differences between adjacent
+  // transitions are always representable, while also guaranteeing that
+  // the arithmetic used to shift between 400-year cycles never overflows.
+  // All valid zones easily meet this requirement.
+  if (transitions_.back().unix_time < 0) return false;
+
   // Extend the transitions for an additional 401 years using the future
   // specification. Years beyond those can be handled by mapping back to
   // a cycle-equivalent year within that range. Note that we need 401
@@ -382,18 +399,45 @@
 
 using FilePtr = std::unique_ptr<FILE, int (*)(FILE*)>;
 
-// fopen(3) adaptor.
-inline FilePtr FOpen(const char* path, const char* mode) {
+// fopen(3) adaptor for reading zoneinfo files (read-only binary mode).
+inline FilePtr FOpen(const char* path) {
 #if defined(_MSC_VER)
   FILE* fp;
-  if (fopen_s(&fp, path, mode) != 0) fp = nullptr;
+  if (fopen_s(&fp, path, "rb") != 0) fp = nullptr;
   return FilePtr(fp, fclose);
 #else
-  // TODO: Enable the close-on-exec flag.
-  return FilePtr(fopen(path, mode), fclose);
+  // Open non-blocking and verify the target is a regular file before handing it
+  // to stdio. Zone names are potentially attacker-controlled, and a plain
+  // fopen() on a FIFO or device node (reachable via the "file:" prefix or an
+  // absolute path) would block indefinitely or read unbounded data.
+  const int fd = open(path, O_RDONLY | O_NONBLOCK | O_CLOEXEC);
+  if (fd >= 0) {
+    struct stat st;
+    if (fstat(fd, &st) == 0 && S_ISREG(st.st_mode)) {
+      FILE* fp = fdopen(fd, "rb");
+      if (fp != nullptr) return FilePtr(fp, fclose);
+    }
+    close(fd);
+  }
+  return FilePtr(nullptr, fclose);
 #endif
 }
 
+// Returns true if the zone name starting at pos contains an unsafe path.
+inline bool UnsafePath(const std::string& name, std::size_t pos) {
+  // Path traversal: exact match ".."
+  if (name.compare(pos, std::string::npos, "..") == 0) return true;
+  // Path traversal: leading component "../"
+  if (name.compare(pos, 3, "../") == 0) return true;
+  // Path traversal: interior component "/../"
+  if (name.find("/../", pos) != std::string::npos) return true;
+  // Path traversal: trailing component "/.."
+  if (name.size() - pos >= 3 && name.compare(name.size() - 3, 3, "/..") == 0) {
+    return true;
+  }
+  return false;
+}
+
 // A stdio(3)-backed implementation of ZoneInfoSource.
 class FileZoneInfoSource : public ZoneInfoSource {
  public:
@@ -431,6 +475,11 @@
   // Use of the "file:" prefix is intended for testing purposes only.
   const std::size_t pos = (name.compare(0, 5, "file:") == 0) ? 5 : 0;
 
+  // Reject unsafe paths (e.g., "../../etc/passwd").
+  if (UnsafePath(name, pos)) {
+    return nullptr;
+  }
+
   // Map the time-zone name to a path name.
   std::string path;
   if (pos == name.size() || name[pos] != '/') {
@@ -451,7 +500,7 @@
   path.append(name, pos, std::string::npos);
 
   // Open the zoneinfo file.
-  auto fp = FOpen(path.c_str(), "rb");
+  auto fp = FOpen(path.c_str());
   if (fp == nullptr) return nullptr;
   return std::unique_ptr<ZoneInfoSource>(new FileZoneInfoSource(std::move(fp)));
 }
@@ -477,7 +526,7 @@
   for (const char* tzdata : {"/apex/com.android.tzdata/etc/tz/tzdata",
                              "/data/misc/zoneinfo/current/tzdata",
                              "/system/usr/share/zoneinfo/tzdata"}) {
-    auto fp = FOpen(tzdata, "rb");
+    auto fp = FOpen(tzdata);
     if (fp == nullptr) continue;
 
     char hbuf[24];  // covers header.zonetab_offset too
@@ -497,9 +546,12 @@
     if (zonecnt * sizeof(ebuf) != index_size) continue;
     for (std::size_t i = 0; i != zonecnt; ++i) {
       if (fread(ebuf, 1, sizeof(ebuf), fp.get()) != sizeof(ebuf)) break;
-      const std::int_fast32_t start = data_offset + Decode32(ebuf + 40);
+      const std::int_fast64_t start =
+          std::int_fast64_t{data_offset} + Decode32(ebuf + 40);
       const std::int_fast32_t length = Decode32(ebuf + 44);
       if (start < 0 || length < 0) break;
+      // fseek() takes a long
+      if (start > std::numeric_limits<long>::max()) break;
       ebuf[40] = '\0';  // ensure zone name is NUL terminated
       if (strcmp(name.c_str() + pos, ebuf) == 0) {
         if (fseek(fp.get(), static_cast<long>(start), SEEK_SET) != 0) break;
@@ -537,6 +589,11 @@
   // Use of the "file:" prefix is intended for testing purposes only.
   const std::size_t pos = (name.compare(0, 5, "file:") == 0) ? 5 : 0;
 
+  // Reject unsafe paths (e.g., "../../etc/passwd").
+  if (UnsafePath(name, pos)) {
+    return nullptr;
+  }
+
   // Prefixes where a Fuchsia component might find zoneinfo files,
   // in descending order of preference.
   const auto kTzdataPrefixes = {
@@ -561,7 +618,7 @@
     if (!prefix.empty()) path += "zoneinfo/tzif2/";  // format
     path.append(name, pos, std::string::npos);
 
-    auto fp = FOpen(path.c_str(), "rb");
+    auto fp = FOpen(path.c_str());
     if (fp == nullptr) continue;
 
     std::string version;
@@ -662,6 +719,12 @@
   if (hdr.ttisstdcnt != 0 && hdr.ttisstdcnt != hdr.typecnt) return false;
   if (hdr.ttisutcnt != 0 && hdr.ttisutcnt != hdr.typecnt) return false;
 
+  // Bound the header counts before sizing tbuf so that a hostile TZif blob
+  // cannot force a very large zero-filled allocation from a tiny input.
+  if (hdr.timecnt > TZ_MAX_TIMES) return false;
+  if (hdr.typecnt > TZ_MAX_TYPES) return false;
+  if (hdr.charcnt > TZ_MAX_CHARS) return false;
+
   // Read the data into a local buffer.
   std::size_t len = hdr.DataLength(time_len);
   std::vector<char> tbuf(len);
@@ -674,6 +737,14 @@
   for (std::size_t i = 0; i != hdr.timecnt; ++i) {
     transitions_[i].unix_time = (time_len == 4) ? Decode32(bp) : Decode64(bp);
     bp += time_len;
+    // A valid zoneinfo file keeps transition times far from the int64 limits.
+    // A hostile one can place them at the extremes, where the
+    // reverse-conversion arithmetic in MakeTime() (tr.unix_time +/- a sub-day
+    // civil delta, see MakeSkipped()/MakeRepeated()) overflows. Bound them to
+    // +/-(1<<59), the times used by the no-op transitions added below.
+    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]))
@@ -705,16 +776,22 @@
   // Determine the before-first-transition type.
   default_transition_type_ = 0;
   if (seen_type_0 && hdr.timecnt != 0) {
-    std::uint_fast8_t index = 0;
+    std::size_t index = 0;
     if (transition_types_[0].is_dst) {
       index = transitions_[0].type_index;
       while (index != 0 && transition_types_[index].is_dst) --index;
     }
     while (index != hdr.typecnt && transition_types_[index].is_dst) ++index;
-    if (index != hdr.typecnt) default_transition_type_ = index;
+    if (index != hdr.typecnt)
+      default_transition_type_ = static_cast<std::uint_fast8_t>(index);
   }
 
-  // Copy all the abbreviations.
+  // Copy all the abbreviations. The area holds NUL-terminated strings, and
+  // LocalTime() hands out a pointer into it, so the final abbreviation has
+  // to be terminated within the area itself. Otherwise an abbreviation runs
+  // on into whatever ExtendTransitions() later appends. (hdr.charcnt != 0
+  // because every abbr_index was validated to be less than it.)
+  if (bp[hdr.charcnt - 1] != '\0') return false;
   abbreviations_.reserve(hdr.charcnt + 10);
   abbreviations_.assign(bp, hdr.charcnt);
   bp += hdr.charcnt;
@@ -771,6 +848,7 @@
   // previous transition is always representable, without overflow.
   const Transition& last(transitions_.back());
   if (last.unix_time < 0) {
+    assert(!extended_);
     const std::uint_fast8_t type_index = last.type_index;
     Transition& tr(*transitions_.emplace(transitions_.end()));
     tr.unix_time = 2147483647;  // 2038-01-19T03:14:07+00:00
diff --git a/absl/time/internal/cctz/src/time_zone_lookup_test.cc b/absl/time/internal/cctz/src/time_zone_lookup_test.cc
index cd08a35..1147e93 100644
--- a/absl/time/internal/cctz/src/time_zone_lookup_test.cc
+++ b/absl/time/internal/cctz/src/time_zone_lookup_test.cc
@@ -15,21 +15,27 @@
 #include <chrono>
 #include <cstddef>
 #include <cstdlib>
+#include <cstring>
 #include <future>
 #include <limits>
+#include <memory>
 #include <string>
 #include <thread>
+#include <utility>
 #include <vector>
 
 #include "absl/base/config.h"
 #include "absl/time/internal/cctz/include/cctz/time_zone.h"
+
 #if defined(__linux__)
 #include <features.h>
 #endif
 
 #include "gtest/gtest.h"
 #include "absl/time/internal/cctz/include/cctz/civil_time.h"
+#include "absl/time/internal/cctz/include/cctz/zone_info_source.h"
 #include "absl/time/internal/cctz/src/test_time_zone_names.h"
+#include "absl/time/internal/cctz/src/tzfile.h"
 
 namespace chrono = std::chrono;
 
@@ -180,6 +186,24 @@
   EXPECT_FALSE(load_time_zone("", &tz));
   EXPECT_EQ(chrono::system_clock::from_time_t(0),
             convert(civil_second(1970, 1, 1, 0, 0, 0), tz));  // UTC
+
+  // Reject path-traversal components.
+  EXPECT_FALSE(load_time_zone("file:../etc/passwd", &tz));
+  EXPECT_FALSE(load_time_zone("file:../../etc/passwd", &tz));
+  EXPECT_FALSE(load_time_zone("file:/../etc/passwd", &tz));
+  EXPECT_FALSE(load_time_zone("file:America/../America/Los_Angeles", &tz));
+
+  // Reject a fixed-offset name with a NUL where a digit belongs.
+  for (const int i : {10, 11, 13, 14, 16, 17}) {
+    std::string name = "Fixed/UTC+00:00:00";
+    name[static_cast<std::size_t>(i)] = '\0';
+    EXPECT_FALSE(load_time_zone(name, &tz)) << "NUL at offset " << i;
+  }
+
+  // Reject non-regular files and directories.
+  EXPECT_FALSE(load_time_zone("file:/dev/null", &tz));
+  EXPECT_FALSE(load_time_zone("file:/dev/stdin", &tz));
+  EXPECT_FALSE(load_time_zone("file:/tmp", &tz));
 }
 
 TEST(TimeZone, Equality) {
@@ -912,6 +936,181 @@
   ExpectTime(tp, tz, 10000, 1, 1, 0, 0, 0, 0 * 3600, false, "UTC");
 }
 
+// A ZoneInfoSource implementation backed by an in-memory string buffer.
+class StringZoneInfoSource : public ZoneInfoSource {
+ public:
+  explicit StringZoneInfoSource(std::string data)
+      : data_(std::move(data)), offset_(0) {}
+
+  std::size_t Read(void* ptr, std::size_t size) override {
+    std::size_t n = (std::min)(size, data_.size() - offset_);
+    std::memcpy(ptr, data_.data() + offset_, n);
+    offset_ += n;
+    return n;
+  }
+
+  int Skip(std::size_t offset) override {
+    if (offset > data_.size() - offset_) return -1;
+    offset_ += offset;
+    return 0;
+  }
+
+ private:
+  std::string data_;
+  std::size_t offset_;
+};
+
+// Constructs a minimal TZif2 string with a single 64-bit transition
+// at the given transition time and a future POSIX rule. The abbreviation
+// area holds abbr verbatim, so a valid file's abbr must include the
+// trailing '\0' (e.g., std::string{"EST", 4}).
+std::string MakeExtendedTzif(std::int_fast64_t unix_time,
+                             std::int_fast32_t utc_offset,
+                             const std::string& abbr,
+                             const std::string& future_spec) {
+  std::string s;
+  auto append32 = [&s](std::int_fast32_t v) {
+    const std::int_fast32_t s32max = 0x7fffffff;
+    const auto s32maxU = static_cast<std::uint_fast32_t>(s32max);
+    std::uint_fast32_t uv;
+    if (v >= 0) {
+      uv = static_cast<std::uint_fast32_t>(v);
+    } else {
+      uv = static_cast<std::uint_fast32_t>(v + s32max + 1) + s32maxU + 1;
+    }
+    for (int i = 3; i >= 0; --i) {
+      s.push_back(static_cast<char>((uv >> (i * 8)) & 0xff));
+    }
+  };
+  auto append64 = [&s](std::int_fast64_t v) {
+    const std::int_fast64_t s64max = 0x7fffffffffffffff;
+    const auto s64maxU = static_cast<std::uint_fast64_t>(s64max);
+    std::uint_fast64_t uv;
+    if (v >= 0) {
+      uv = static_cast<std::uint_fast64_t>(v);
+    } else {
+      uv = static_cast<std::uint_fast64_t>(v + s64max + 1) + s64maxU + 1;
+    }
+    for (int i = 7; i >= 0; --i) {
+      s.push_back(static_cast<char>((uv >> (i * 8)) & 0xff));
+    }
+  };
+
+  const std::size_t charcnt = abbr.size();
+
+  // 32-bit header
+  s.append(TZ_MAGIC, 4);
+  s.push_back('2');    // tzh_version
+  s.append(15, '\0');  // tzh_reserved
+  append32(0);         // tzh_ttisutcnt
+  append32(0);         // tzh_ttisstdcnt
+  append32(0);         // tzh_leapcnt
+  append32(0);         // tzh_timecnt (0 32-bit transitions)
+  append32(1);         // tzh_typecnt (1 ttinfo record)
+  append32(static_cast<std::int_fast32_t>(charcnt));  // tzh_charcnt
+
+  // 32-bit data block
+  append32(utc_offset);  // tt_utoff
+  s.push_back(0);        // tt_isdst (standard time)
+  s.push_back(0);        // tt_desigidx
+  s.append(abbr);        // abbreviation table
+
+  // 64-bit header
+  s.append(TZ_MAGIC, 4);
+  s.push_back('2');    // tzh_version
+  s.append(15, '\0');  // tzh_reserved
+  append32(0);         // tzh_ttisutcnt
+  append32(0);         // tzh_ttisstdcnt
+  append32(0);         // tzh_leapcnt
+  append32(1);         // tzh_timecnt (1 64-bit transition)
+  append32(1);         // tzh_typecnt (1 ttinfo record)
+  append32(static_cast<std::int_fast32_t>(charcnt));  // tzh_charcnt
+
+  // 64-bit data block
+  append64(unix_time);   // transition time
+  s.push_back(0);        // type index for transition
+  append32(utc_offset);  // tt_utoff
+  s.push_back(0);        // tt_isdst (standard time)
+  s.push_back(0);        // tt_desigidx
+  s.append(abbr);        // abbreviation table
+
+  // POSIX footer
+  s.push_back('\n');
+  s.append(future_spec);
+  s.push_back('\n');
+  return s;
+}
+
+std::unique_ptr<ZoneInfoSource> ExtendedTestFactory(
+    const std::string& name,
+    const std::function<std::unique_ptr<ZoneInfoSource>(const std::string&)>&
+        fallback) {
+  if (name == "test:ExtendedBeforeEpoch") {
+    // -1 (1969-12-31T23:59:59Z) is the latest final transition before the
+    // epoch, so the zone is rejected despite the future specification.
+    return std::unique_ptr<ZoneInfoSource>(
+        new StringZoneInfoSource(MakeExtendedTzif(
+            -1, -5 * 3600, std::string{"EST", 4}, "EST5EDT,M3.2.0,M11.1.0")));
+  }
+  if (name == "test:ExtendedFarFuture") {
+    // 0 (1970-01-01T00:00:00Z) is the earliest final transition an extended
+    // zone may have, which maximizes the 400-year shift that BreakTime()
+    // needs for a lookup at the maximum time.
+    return std::unique_ptr<ZoneInfoSource>(
+        new StringZoneInfoSource(MakeExtendedTzif(
+            0, -5 * 3600, std::string{"EST", 4}, "EST5EDT,M3.2.0,M11.1.0")));
+  }
+  if (name == "test:UnterminatedAbbreviation") {
+    // The abbreviation area is missing its final NUL, so the abbreviation
+    // would run into whatever ExtendTransitions() appends behind it.
+    return std::unique_ptr<ZoneInfoSource>(new StringZoneInfoSource(
+        MakeExtendedTzif(0, -5 * 3600, "EST", "EST5EDT,M3.2.0,M11.1.0")));
+  }
+  return fallback(name);
+}
+
+// Tests that a TZif file whose abbreviation area is not NUL-terminated
+// is rejected.
+TEST(TimeZoneEdgeCase, UnterminatedAbbreviation) {
+  auto prev_factory = cctz_extension::zone_info_source_factory;
+  cctz_extension::zone_info_source_factory = ExtendedTestFactory;
+
+  time_zone tz;
+  EXPECT_FALSE(load_time_zone("test:UnterminatedAbbreviation", &tz));
+
+  cctz_extension::zone_info_source_factory = prev_factory;
+}
+
+// Tests that a TZif file whose explicit transitions end before epoch
+// is rejected when it has a POSIX DST footer string.
+TEST(TimeZoneEdgeCase, ExtendedBeforeEpoch) {
+  auto prev_factory = cctz_extension::zone_info_source_factory;
+  cctz_extension::zone_info_source_factory = ExtendedTestFactory;
+
+  // Extended zones must end with a non-negative explicit transition.
+  time_zone tz;
+  EXPECT_FALSE(load_time_zone("test:ExtendedBeforeEpoch", &tz));
+
+  cctz_extension::zone_info_source_factory = prev_factory;
+}
+
+// Looking up the maximum time in an extended zone must fold back through the
+// 400-year cycle without overflowing when BreakTime() computes the shift.
+TEST(TimeZoneEdgeCase, ExtendedFarFuture) {
+  auto prev_factory = cctz_extension::zone_info_source_factory;
+  cctz_extension::zone_info_source_factory = ExtendedTestFactory;
+
+  time_zone tz;
+  ASSERT_TRUE(load_time_zone("test:ExtendedFarFuture", &tz));
+
+  auto tp_max = time_point<absl::time_internal::cctz::seconds>::max();
+  ExpectTime(tp_max, tz, 292277026596, 12, 4, 10, 30, 7, -5 * 3600, false,
+             "EST");
+  EXPECT_STREQ("EST", tz.lookup(tp_max).abbr);
+
+  cctz_extension::zone_info_source_factory = prev_factory;
+}
+
 }  // namespace cctz
 }  // namespace time_internal
 ABSL_NAMESPACE_END
diff --git a/absl/time/internal/cctz/src/time_zone_posix.cc b/absl/time/internal/cctz/src/time_zone_posix.cc
index efea080..c60f98b 100644
--- a/absl/time/internal/cctz/src/time_zone_posix.cc
+++ b/absl/time/internal/cctz/src/time_zone_posix.cc
@@ -92,14 +92,17 @@
   return p;
 }
 
-// datetime = ( Jn | n | Mm.w.d ) [ / offset ]
+// datetime = , ( Jn | n | Mm.w.d ) [ / offset ]
 const char* ParseDateTime(const char* p, PosixTransition* res) {
-  if (p != nullptr && *p == ',') {
+  if (p != nullptr) {
+    if (*p != ',') return nullptr;
     if (*++p == 'M') {
       int month = 0;
-      if ((p = ParseInt(p + 1, 1, 12, &month)) != nullptr && *p == '.') {
+      if ((p = ParseInt(p + 1, 1, 12, &month)) != nullptr) {
+        if (*p != '.') return nullptr;
         int week = 0;
-        if ((p = ParseInt(p + 1, 1, 5, &week)) != nullptr && *p == '.') {
+        if ((p = ParseInt(p + 1, 1, 5, &week)) != nullptr) {
+          if (*p != '.') return nullptr;
           int weekday = 0;
           if ((p = ParseInt(p + 1, 0, 6, &weekday)) != nullptr) {
             res->date.fmt = PosixTransition::M;
@@ -122,17 +125,17 @@
         res->date.n.day = static_cast<std::int_fast16_t>(day);
       }
     }
-  }
-  if (p != nullptr) {
-    res->time.offset = 2 * 60 * 60;  // default offset is 02:00:00
-    if (*p == '/') p = ParseOffset(p + 1, -167, 167, 1, &res->time.offset);
+    if (p != nullptr) {
+      res->time.offset = 2 * 60 * 60;  // default offset is 02:00:00
+      if (*p == '/') p = ParseOffset(p + 1, -167, 167, 1, &res->time.offset);
+    }
   }
   return p;
 }
 
 }  // namespace
 
-// spec = std offset [ dst [ offset ] , datetime , datetime ]
+// spec = std offset [ dst [ offset ] datetime datetime ]
 bool ParsePosixSpec(const std::string& spec, PosixTimeZone* res) {
   const char* p = spec.c_str();
   if (*p == ':') return false;
diff --git a/absl/time/internal/cctz/src/time_zone_posix.h b/absl/time/internal/cctz/src/time_zone_posix.h
index 7fd2b9e..23f5699 100644
--- a/absl/time/internal/cctz/src/time_zone_posix.h
+++ b/absl/time/internal/cctz/src/time_zone_posix.h
@@ -13,7 +13,7 @@
 //   limitations under the License.
 
 // Parsing of a POSIX zone spec as described in the TZ part of section 8.3 in
-// http://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap08.html.
+// https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/V1_chap08.html.
 //
 // The current POSIX spec for America/Los_Angeles is "PST8PDT,M3.2.0,M11.1.0",
 // which would be broken down as ...
diff --git a/absl/time/internal/cctz/src/time_zone_posix_test.cc b/absl/time/internal/cctz/src/time_zone_posix_test.cc
new file mode 100644
index 0000000..8e429f7
--- /dev/null
+++ b/absl/time/internal/cctz/src/time_zone_posix_test.cc
@@ -0,0 +1,195 @@
+// Copyright 2026 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//   https://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+
+#include "absl/time/internal/cctz/src/time_zone_posix.h"
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "absl/base/config.h"
+
+namespace absl {
+ABSL_NAMESPACE_BEGIN
+namespace time_internal {
+namespace cctz {
+
+using ::testing::Eq;
+using ::testing::IsEmpty;
+
+// We only support the second POSIX format (that is, neither the
+// "first character is a <colon>" format, nor the "geographical
+// or a special timezone" format).  We also require DST start/end
+// rules whenever a DST abbreviation is given (zic always provides
+// them).  So, ...
+//
+//   spec = abbr offset [ abbr [ offset ] datetime datetime ]
+//   abbr = <.*?> | [^-+,\d]{3,}
+//   offset = [+|-]hh[:mm[:ss]]
+//   datetime = , ( Jn | n | Mm.w.d ) [ / offset ]
+
+TEST(ParsePosixSpec, UnsupportedFormats) {
+  PosixTimeZone zone;
+  EXPECT_FALSE(ParsePosixSpec(":characters", &zone));
+  EXPECT_FALSE(ParsePosixSpec("Area/Location", &zone));
+}
+
+TEST(ParsePosixSpec, StdOnly) {
+  PosixTimeZone zone;
+
+  // America/Cancun
+  EXPECT_TRUE(ParsePosixSpec("EST5", &zone));
+  EXPECT_THAT(zone.std_abbr, Eq("EST"));
+  EXPECT_THAT(zone.std_offset, Eq(-5 * 60 * 60));
+  EXPECT_THAT(zone.dst_abbr, IsEmpty());
+
+  // Australia/Darwin
+  EXPECT_TRUE(ParsePosixSpec("ACST-9:30", &zone));
+  EXPECT_THAT(zone.std_abbr, Eq("ACST"));
+  EXPECT_THAT(zone.std_offset, Eq((9 * 60 + 30) * 60));
+  EXPECT_THAT(zone.dst_abbr, IsEmpty());
+
+  // Australia/Eucla
+  EXPECT_TRUE(ParsePosixSpec("<+0845>-8:45", &zone));
+  EXPECT_THAT(zone.std_abbr, Eq("+0845"));
+  EXPECT_THAT(zone.std_offset, Eq((8 * 60 + 45) * 60));
+  EXPECT_THAT(zone.dst_abbr, IsEmpty());
+}
+
+TEST(ParsePosixSpec, WithDst) {
+  PosixTimeZone zone;
+
+  // America/New_York
+  EXPECT_TRUE(ParsePosixSpec("EST5EDT,M3.2.0,M11.1.0", &zone));
+  EXPECT_THAT(zone.std_abbr, Eq("EST"));
+  EXPECT_THAT(zone.std_offset, Eq(-5 * 60 * 60));
+  EXPECT_THAT(zone.dst_abbr, Eq("EDT"));
+  EXPECT_THAT(zone.dst_offset, Eq(-4 * 60 * 60));
+  EXPECT_THAT(zone.dst_start.date.fmt, Eq(PosixTransition::M));
+  EXPECT_THAT(zone.dst_start.date.m.month, Eq(3));
+  EXPECT_THAT(zone.dst_start.date.m.week, Eq(2));
+  EXPECT_THAT(zone.dst_start.date.m.weekday, Eq(0));
+  EXPECT_THAT(zone.dst_start.time.offset, Eq(2 * 60 * 60));
+  EXPECT_THAT(zone.dst_end.date.fmt, Eq(PosixTransition::M));
+  EXPECT_THAT(zone.dst_end.date.m.month, Eq(11));
+  EXPECT_THAT(zone.dst_end.date.m.week, Eq(1));
+  EXPECT_THAT(zone.dst_end.date.m.weekday, Eq(0));
+  EXPECT_THAT(zone.dst_end.time.offset, Eq(2 * 60 * 60));
+
+  // Australia/Adelaide
+  EXPECT_TRUE(ParsePosixSpec("ACST-9:30ACDT,M10.1.0,M4.1.0/3", &zone));
+  EXPECT_THAT(zone.std_abbr, Eq("ACST"));
+  EXPECT_THAT(zone.std_offset, Eq((9 * 60 + 30) * 60));
+  EXPECT_THAT(zone.dst_abbr, Eq("ACDT"));
+  EXPECT_THAT(zone.dst_offset, Eq((10 * 60 + 30) * 60));
+  EXPECT_THAT(zone.dst_start.date.fmt, Eq(PosixTransition::M));
+  EXPECT_THAT(zone.dst_start.date.m.month, Eq(10));
+  EXPECT_THAT(zone.dst_start.date.m.week, Eq(1));
+  EXPECT_THAT(zone.dst_start.date.m.weekday, Eq(0));
+  EXPECT_THAT(zone.dst_start.time.offset, Eq(2 * 60 * 60));
+  EXPECT_THAT(zone.dst_end.date.fmt, Eq(PosixTransition::M));
+  EXPECT_THAT(zone.dst_end.date.m.month, Eq(4));
+  EXPECT_THAT(zone.dst_end.date.m.week, Eq(1));
+  EXPECT_THAT(zone.dst_end.date.m.weekday, Eq(0));
+  EXPECT_THAT(zone.dst_end.time.offset, Eq(3 * 60 * 60));
+
+  // Australia/Lord_Howe
+  EXPECT_TRUE(ParsePosixSpec("<+1030>-10:30<+11>-11,M10.1.0,M4.1.0", &zone));
+  EXPECT_THAT(zone.std_abbr, Eq("+1030"));
+  EXPECT_THAT(zone.std_offset, Eq((10 * 60 + 30) * 60));
+  EXPECT_THAT(zone.dst_abbr, Eq("+11"));
+  EXPECT_THAT(zone.dst_offset, Eq(11 * 60 * 60));
+  EXPECT_THAT(zone.dst_start.date.fmt, Eq(PosixTransition::M));
+  EXPECT_THAT(zone.dst_start.date.m.month, Eq(10));
+  EXPECT_THAT(zone.dst_start.date.m.week, Eq(1));
+  EXPECT_THAT(zone.dst_start.date.m.weekday, Eq(0));
+  EXPECT_THAT(zone.dst_start.time.offset, Eq(2 * 60 * 60));
+  EXPECT_THAT(zone.dst_end.date.fmt, Eq(PosixTransition::M));
+  EXPECT_THAT(zone.dst_end.date.m.month, Eq(4));
+  EXPECT_THAT(zone.dst_end.date.m.week, Eq(1));
+  EXPECT_THAT(zone.dst_end.date.m.weekday, Eq(0));
+  EXPECT_THAT(zone.dst_end.time.offset, Eq(2 * 60 * 60));
+
+  // Africa/Casablanca (year-round DST)
+  EXPECT_TRUE(ParsePosixSpec("<+00>0<+01>,0/0,J365/25", &zone));
+  EXPECT_THAT(zone.std_abbr, Eq("+00"));
+  EXPECT_THAT(zone.std_offset, Eq(0));
+  EXPECT_THAT(zone.dst_abbr, Eq("+01"));
+  EXPECT_THAT(zone.dst_offset, Eq(1 * 60 * 60));
+  EXPECT_THAT(zone.dst_start.date.fmt, Eq(PosixTransition::N));
+  EXPECT_THAT(zone.dst_start.date.n.day, Eq(0));
+  EXPECT_THAT(zone.dst_start.time.offset, Eq(0));
+  EXPECT_THAT(zone.dst_end.date.fmt, Eq(PosixTransition::J));
+  EXPECT_THAT(zone.dst_end.date.n.day, Eq(365));
+  EXPECT_THAT(zone.dst_end.time.offset, Eq(25 * 60 * 60));
+}
+
+TEST(TimeZonePosix, ParseErrors) {
+  PosixTimeZone zone;
+
+  // STD abbreviation errors.
+  EXPECT_FALSE(ParsePosixSpec("ET5", &zone));
+  EXPECT_FALSE(ParsePosixSpec("ET+", &zone));
+  EXPECT_FALSE(ParsePosixSpec("ET-", &zone));
+  EXPECT_FALSE(ParsePosixSpec("ET,", &zone));
+  EXPECT_FALSE(ParsePosixSpec("<00", &zone));
+
+  // STD offset errors.
+  EXPECT_FALSE(ParsePosixSpec("<00>", &zone));
+  EXPECT_FALSE(ParsePosixSpec("<00>+", &zone));
+  EXPECT_FALSE(ParsePosixSpec("<00>-", &zone));
+  EXPECT_FALSE(ParsePosixSpec("<00>?", &zone));
+
+  // DST abbreviation errors.
+  EXPECT_FALSE(ParsePosixSpec("EST5DT,M3.2.0,M11.1.0", &zone));
+  EXPECT_FALSE(ParsePosixSpec("EST5EDT+,M3.2.0,M11.1.0", &zone));
+  EXPECT_FALSE(ParsePosixSpec("EST5EDT-,M3.2.0,M11.1.0", &zone));
+  EXPECT_FALSE(ParsePosixSpec("<00>0<-01", &zone));
+
+  // DST offset errors.
+  EXPECT_FALSE(ParsePosixSpec("<01>1<00>?,0,0", &zone));
+  EXPECT_FALSE(ParsePosixSpec("<01>1<00>+?,0,0", &zone));
+  EXPECT_FALSE(ParsePosixSpec("<01>1<00>-?,0,0", &zone));
+
+  // Malformed DST start date/time.
+  EXPECT_FALSE(ParsePosixSpec("EST5EDT", &zone));
+  EXPECT_FALSE(ParsePosixSpec("EST5EDT,", &zone));
+  EXPECT_FALSE(ParsePosixSpec("EST5EDT,M13.2.0,M11.1.0", &zone));
+  EXPECT_FALSE(ParsePosixSpec("EST5EDT,M3.6.0,M11.1.0", &zone));
+  EXPECT_FALSE(ParsePosixSpec("EST5EDT,M3.2.7,M11.1.0", &zone));
+  EXPECT_FALSE(ParsePosixSpec("EST5EDT,J0,M11.1.0", &zone));
+  EXPECT_FALSE(ParsePosixSpec("EST5EDT,366,M11.1.0", &zone));
+  EXPECT_FALSE(ParsePosixSpec("EST5EDT,M3.2.0/,M11.1.0", &zone));
+  EXPECT_FALSE(ParsePosixSpec("EST5EDT,M3.2.0/?,M11.1.0", &zone));
+  EXPECT_FALSE(ParsePosixSpec("EST5EDT,M3.2.0/1:?,M11.1.0", &zone));
+  EXPECT_FALSE(ParsePosixSpec("EST5EDT,M3.2.0/1:2:?,M11.1.0", &zone));
+
+  // Malformed DST end date/time.
+  EXPECT_FALSE(ParsePosixSpec("EST5EDT,M3.2.0", &zone));
+  EXPECT_FALSE(ParsePosixSpec("EST5EDT,M3.2.0,M?.1.0", &zone));
+  EXPECT_FALSE(ParsePosixSpec("EST5EDT,M3.2.0,M11.?.0", &zone));
+  EXPECT_FALSE(ParsePosixSpec("EST5EDT,M3.2.0,M11.1.?", &zone));
+  EXPECT_FALSE(ParsePosixSpec("EST5EDT,M3.2.0,J?", &zone));
+  EXPECT_FALSE(ParsePosixSpec("EST5EDT,M3.2.0,?", &zone));
+  EXPECT_FALSE(ParsePosixSpec("EST5EDT,M3.2.0,M11.1.0/", &zone));
+  EXPECT_FALSE(ParsePosixSpec("EST5EDT,M3.2.0,M11.1.0/168", &zone));
+  EXPECT_FALSE(ParsePosixSpec("EST5EDT,M3.2.0,M11.1.0/167:60", &zone));
+  EXPECT_FALSE(ParsePosixSpec("EST5EDT,M3.2.0,M11.1.0/167:59:60", &zone));
+
+  // Trailing data.
+  EXPECT_FALSE(ParsePosixSpec("EST5EDT,M3.2.0,M11.1.0junk", &zone));
+}
+
+}  // namespace cctz
+}  // namespace time_internal
+ABSL_NAMESPACE_END
+}  // namespace absl
diff --git a/absl/time/internal/cctz/testdata/version b/absl/time/internal/cctz/testdata/version
index 75d34ee..9217a2d 100644
--- a/absl/time/internal/cctz/testdata/version
+++ b/absl/time/internal/cctz/testdata/version
@@ -1 +1 @@
-2026b
+2026c
diff --git a/absl/time/internal/cctz/testdata/zoneinfo/Africa/Casablanca b/absl/time/internal/cctz/testdata/zoneinfo/Africa/Casablanca
index 240ebb2..fb2f5cc 100644
--- a/absl/time/internal/cctz/testdata/zoneinfo/Africa/Casablanca
+++ b/absl/time/internal/cctz/testdata/zoneinfo/Africa/Casablanca
Binary files differ
diff --git a/absl/time/internal/cctz/testdata/zoneinfo/Africa/El_Aaiun b/absl/time/internal/cctz/testdata/zoneinfo/Africa/El_Aaiun
index 909c5f9..46286b9 100644
--- a/absl/time/internal/cctz/testdata/zoneinfo/Africa/El_Aaiun
+++ b/absl/time/internal/cctz/testdata/zoneinfo/Africa/El_Aaiun
Binary files differ
diff --git a/absl/time/internal/cctz/testdata/zoneinfo/America/Edmonton b/absl/time/internal/cctz/testdata/zoneinfo/America/Edmonton
index 645ee94..379e365 100644
--- a/absl/time/internal/cctz/testdata/zoneinfo/America/Edmonton
+++ b/absl/time/internal/cctz/testdata/zoneinfo/America/Edmonton
Binary files differ
diff --git a/absl/time/internal/cctz/testdata/zoneinfo/America/Yellowknife b/absl/time/internal/cctz/testdata/zoneinfo/America/Yellowknife
index 645ee94..379e365 100644
--- a/absl/time/internal/cctz/testdata/zoneinfo/America/Yellowknife
+++ b/absl/time/internal/cctz/testdata/zoneinfo/America/Yellowknife
Binary files differ
diff --git a/absl/time/internal/cctz/testdata/zoneinfo/Canada/Mountain b/absl/time/internal/cctz/testdata/zoneinfo/Canada/Mountain
index 645ee94..379e365 100644
--- a/absl/time/internal/cctz/testdata/zoneinfo/Canada/Mountain
+++ b/absl/time/internal/cctz/testdata/zoneinfo/Canada/Mountain
Binary files differ
diff --git a/absl/time/internal/cctz/testdata/zoneinfo/zone1970.tab b/absl/time/internal/cctz/testdata/zoneinfo/zone1970.tab
index a9b47bc..635eabc 100644
--- a/absl/time/internal/cctz/testdata/zoneinfo/zone1970.tab
+++ b/absl/time/internal/cctz/testdata/zoneinfo/zone1970.tab
@@ -112,7 +112,7 @@
 CA	+624900-0920459	America/Rankin_Inlet	Central - NU (central)
 CA	+5024-10439	America/Regina	CST - SK (most areas)
 CA	+5017-10750	America/Swift_Current	CST - SK (midwest)
-CA	+5333-11328	America/Edmonton	Mountain - AB, BC(E), NT(E), SK(W)
+CA	+5333-11328	America/Edmonton	CST - AB, BC(E), NT(E), SK(W)
 CA	+690650-1050310	America/Cambridge_Bay	Mountain - NU (west)
 CA	+682059-1334300	America/Inuvik	Mountain - NT (west)
 CA	+4916-12307	America/Vancouver	MST - BC (most areas)
diff --git a/absl/time/internal/cctz/testdata/zoneinfo/zonenow.tab b/absl/time/internal/cctz/testdata/zoneinfo/zonenow.tab
index 54e4485..9c3a8cf 100644
--- a/absl/time/internal/cctz/testdata/zoneinfo/zonenow.tab
+++ b/absl/time/internal/cctz/testdata/zoneinfo/zonenow.tab
@@ -56,16 +56,19 @@
 XX	-2504-13005	Pacific/Pitcairn	Pitcairn
 #
 # -08/-07 - PST/PDT (North America DST)
-XX	+340308-1181434	America/Los_Angeles	Pacific (PST/PDT) - US & Canada; Mexico near US border
+XX	+340308-1181434	America/Los_Angeles	Pacific (PST/PDT) - US; Mexico near US border
 #
 # -08/-07 - PST/PDT (North America DST) until 2026-11-01 02:00; then MST
-XX	+4916-12307	America/Vancouver	MST - BC (most areas)
+XX	+4916-12307	America/Vancouver	Mountain Standard (MST) - British Columbia (most areas)
 #
 # -07 - MST
 XX	+332654-1120424	America/Phoenix	Mountain Standard (MST) - Arizona; western Mexico; Yukon
 #
 # -07/-06 - MST/MDT (North America DST)
-XX	+394421-1045903	America/Denver	Mountain (MST/MDT) - US & Canada; Mexico near US border
+XX	+394421-1045903	America/Denver	Mountain (MST/MDT) - US; Mexico near US border; northern Canada
+#
+# -07/-06 - MST/MDT (North America DST) until 2026-11-01 02:00; then CST
+XX	+5333-11328	America/Edmonton	Central Standard (CST) - Alberta and some neighbors
 #
 # -06
 XX	-0054-08936	Pacific/Galapagos	Galápagos