Skip to content

Fix: DateTimeParser trailing timezone designators & Timezone UTC offset cache invalidation#5318

Open
kozemcak wants to merge 2 commits intopocoproject:mainfrom
kozemcak:fix/time
Open

Fix: DateTimeParser trailing timezone designators & Timezone UTC offset cache invalidation#5318
kozemcak wants to merge 2 commits intopocoproject:mainfrom
kozemcak:fix/time

Conversation

@kozemcak
Copy link
Copy Markdown

Hi all,

After we update poco to new version our applications tests start failing. We identified these issues and fix it. Some of the issues are little specific, so we are interesting what you say about that. Here are quick summary of our changes.

Thanks.

Summary

This MR contains two bug fixes for the Foundation library addressing datetime parsing and timezone handling regressions.


Changes

1. fix(Foundation): DateTimeParser: allow trailing timezone designators

A strict trailing-garbage check introduced in commit 8410eb1a6 broke parsing of valid date strings with trailing timezone indicators not captured by the format string.

Affected scenarios:

  • ASN.1 UTCTime strings (e.g. "230101120000Z") used by Poco::Crypto::X509Certificate::validFrom() and expiresOn() — caused certificate loading to fail
  • ISO 8601 strings with fractional seconds and numeric UTC offsets (e.g. "2013-10-07 08:23:19.120-04:00") — triggered false SyntaxException

Fix: After skipping trailing whitespace, optionally consume fractional seconds and/or a trailing timezone designator before the trailing-garbage check.


2. fix(Foundation): Timezone: invalidate utcOffset cache when /etc/localtime changes

The TZInfo UTC offset cache introduced in Poco commit 1850dc16 is only invalidated when the TZ environment variable changes. Since TZ is process-local, system timezone changes via /etc/localtime (symlink update) go undetected, causing Timezone::utcOffset() to return a stale value.

Fix: Extend cacheTZ()/tzChanged() to track the inode and mtime of /etc/localtime via stat(2). Cache is invalidated when either changes.


Testing

  • Verified ASN.1 UTCTime strings parse correctly
  • Verified ISO 8601 strings with fractional seconds and UTC offsets parse correctly
  • Verified utcOffset() reflects timezone changes after /etc/localtime is updated without modifying TZ

Commit 8410eb1 ("fix(Foundation): Reject trailing garbage in
DateTimeParser (pocoproject#5030) (pocoproject#5117)") added a strict check that throws
SyntaxException when any characters remain unconsumed after parsing.
This breaks date strings that carry a trailing timezone indicator that
is not captured by the format string -- a common, valid pattern in many
date-time representations:

 - ISO 8601 / RFC 3339:  "2013-10-07 08:23:19.120-04:00"
 - UTC indicator:        "230101120000Z"  (ASN.1 UTCTime)
 - Named zones:          "Mon, 07 Oct 2013 08:23:19 GMT"

Affected users include:
 - Poco::Crypto::X509Certificate::validFrom() and expiresOn(), which
   parse ASN.1 time strings (e.g. "YYMMDDHHMMSSZ") using
   DateTimeParser::parse() with format "%y%m%d%H%M%S". The trailing
   'Z' was left unconsumed, triggering the exception and causing
   certificate loading to fail.
 - Application code that stores and validates ISO 8601 date strings
   with numeric UTC offsets (e.g. "2013-10-07 08:23:19.120-04:00")
   using DateTimeParser::parse() with ISO8601_FORMAT ("%Y-%m-%dT%H:%M:%S%z").
   The %z specifier calls parseTZD() but parseTZD() returns without
   consuming anything when it sees '.' (fractional seconds); the
   remaining ".120-04:00" then triggers the trailing-garbage check.

Fix: after skipping trailing whitespace:
 1. Skip optional fractional seconds ('.' or ',' followed by digits)
    that were left unconsumed because the format used %S (integer
    seconds) instead of %s (fractional seconds).
 2. Attempt to consume an optional trailing timezone designator using
    the existing parseTZD() helper (handles 'Z', named zones, and
    +/-HH:MM numeric offsets). The iterator is restored if parseTZD()
    throws, so that truly invalid trailing characters are still caught
    by the downstream check.

Signed-off-by: Andrej Kozemcak <andrej.kozemcak@siemens.com>
…time changes

Poco commit 1850dc1 introduced a
TZInfo cache for the UTC offset to avoid repeated tzset() syscalls.
The cache is invalidated only when the TZ environment variable changes.
However, the TZ variable is process-local: if a different process (e.g.
a timezone configuration daemon or an init script) changes the system
timezone by updating /etc/localtime, the running process is not notified
and its TZ environment variable remains unchanged.

On systems that switch timezone by updating /etc/localtime (a symlink)
without touching the TZ env var, the cache is therefore never invalidated
and Timezone::utcOffset() returns the stale value computed at startup.

Fix by extending cacheTZ()/tzChanged() to also track the inode and
mtime of /etc/localtime via stat(2).  When either changes the cache is
considered stale and reloaded, preserving the performance benefit for
the common case where neither TZ nor /etc/localtime changes between
calls.

Signed-off-by: Andrej Kozemcak <andrej.kozemcak@siemens.com>
@matejk
Copy link
Copy Markdown
Contributor

matejk commented Apr 16, 2026

The ASN.1 UTCTime issue (X509Certificate::validFrom()/expiresOn()) is already fixed in #5263 by including the %Z specifier in the format strings on the caller side.

The other example "2013-10-07 08:23:19.120-04:00" is not a valid ISO 8601 datetime -- ISO 8601 requires the T separator between date and time (the 2019 revision removed the earlier allowance to omit it by mutual agreement). POCO's ISO8601_FORMAT correctly requires it.

The real issue is that %S stops at the fractional-second separator, leaving %z unable to reach the timezone. Rather than tolerating trailing garbage after parsing, a better fix is to make %S consume and discard optional fractional seconds:

case 'S':
    it = skipNonDigits(it, end);
    second = parseNumberN(dtStr, it, end, 2);
    if (it != end && (*it == '.' || *it == ','))
    {
        ++it;
        it = skipDigits(it, end);
    }
    break;

This way %z sees the timezone directly and the trailing-garbage check remains fully effective. Could you test this approach?

@kozemcak
Copy link
Copy Markdown
Author

Hi matejk,

Thank you for the answer. Next week I will look at that and I will try your suggestion.

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