Utils::ParseServerTime: int64 overflow on far-future @time= tag #1086

Closed
opened 2026-10-05 05:02:37 +02:00 by adam · 2 comments
Owner

CUtils::ParseServerTime: int64 overflow on far-future @time= tag

CUtils::ParseServerTime hands an attacker-controlled IRCv3 @time= tag
straight to cctz::parse without any range validation. cctz internally calls
chrono::time_point_cast<microseconds> which multiplies the parsed seconds
by 1,000,000. Years past roughly 292,000 (when seconds since the epoch
exceeds INT64_MAX / 1_000_000) cause signed overflow inside libc++.

// src/Utils.cpp
569: timeval CUtils::ParseServerTime(const CString& sTime) {
570:     using namespace std::chrono;
571:     system_clock::time_point tp;
572:     cctz::parse("%Y-%m-%dT%H:%M:%E*SZ", sTime, cctz::utc_time_zone(), &tp);
573:     struct timeval tv;
574:     memset(&tv, 0, sizeof(tv));
575:     microseconds usec = duration_cast<microseconds>(tp.time_since_epoch());
576:     tv.tv_sec = usec.count() / 1000000;
577:     tv.tv_usec = usec.count() % 1000000;
578:     return tv;
579: }

tp.time_since_epoch() is in seconds; the cast at line 575 (and equivalents
inside cctz when it joins seconds + fractional) does seconds * 1_000_000
in int64.

Reachability

@time tags arrive from the IRC server ZNC connects to and end up parsed
via CMessage::InitTime. A malicious or buggy IRC server can send any year
in the tag value.

Production impact: arm64/x86 just wrap the multiply, so tv_sec becomes
garbage and the message ends up with a wrong timestamp in buffer playback.
No crash, no memory corruption.

UBSan / -ftrapv builds: hard abort. Concretely this fires under CIFuzz on
any PR that touches the time-parsing path.

Reproduction

// poc_servertime.cpp
#include <cstdio>
#include <znc/Utils.h>
int main() {
    timeval tv = CUtils::ParseServerTime("202620260-5-25T08:00:00Z");
    fprintf(stderr, "tv_sec=%lld tv_usec=%ld\n",
            (long long)tv.tv_sec, (long)tv.tv_usec);
}

Build:

clang++ -std=c++17 -fsanitize=address,undefined -g -O1 \
  -I include -I build/include -include znc/zncconfig.h \
  poc_servertime.cpp build/src/libznc.a \
  -lpthread -ldl -framework CoreFoundation -L/opt/homebrew/lib -largon2 \
  -o poc_servertime
UBSAN_OPTIONS=halt_on_error=0:print_stacktrace=1 ./poc_servertime

Output:

__chrono/duration.h:93:79: runtime error: signed integer overflow:
  6394015664380800 * 1000000 cannot be represented in type 'long long'
    #0 ... chrono::__duration_cast::operator()
    #3 cctz::detail::join_seconds<long long, 1000000>
    #4 cctz::parse<...>
    #5 CUtils::ParseServerTime  Utils.cpp:572
    #6 main
tv_sec=-7004529196414 tv_usec=-410752

Fix

Validate the seconds range before the cast (or catch it after). Simplest:

timeval CUtils::ParseServerTime(const CString& sTime) {
    using namespace std::chrono;
    system_clock::time_point tp;
    if (!cctz::parse("%Y-%m-%dT%H:%M:%E*SZ", sTime, cctz::utc_time_zone(), &tp))
        return {};
    auto secs = duration_cast<seconds>(tp.time_since_epoch()).count();
    constexpr int64_t kMaxSec = INT64_MAX / 1'000'000;
    if (secs > kMaxSec || secs < -kMaxSec) return {};
    ...
}

Could send a PR with the bound check + a regression test.

# `CUtils::ParseServerTime`: int64 overflow on far-future `@time=` tag `CUtils::ParseServerTime` hands an attacker-controlled IRCv3 `@time=` tag straight to `cctz::parse` without any range validation. cctz internally calls `chrono::time_point_cast<microseconds>` which multiplies the parsed seconds by 1,000,000. Years past roughly 292,000 (when seconds since the epoch exceeds `INT64_MAX / 1_000_000`) cause signed overflow inside libc++. ```cpp // src/Utils.cpp 569: timeval CUtils::ParseServerTime(const CString& sTime) { 570: using namespace std::chrono; 571: system_clock::time_point tp; 572: cctz::parse("%Y-%m-%dT%H:%M:%E*SZ", sTime, cctz::utc_time_zone(), &tp); 573: struct timeval tv; 574: memset(&tv, 0, sizeof(tv)); 575: microseconds usec = duration_cast<microseconds>(tp.time_since_epoch()); 576: tv.tv_sec = usec.count() / 1000000; 577: tv.tv_usec = usec.count() % 1000000; 578: return tv; 579: } ``` `tp.time_since_epoch()` is in seconds; the cast at line 575 (and equivalents inside cctz when it joins seconds + fractional) does `seconds * 1_000_000` in `int64`. ## Reachability `@time` tags arrive from the IRC server ZNC connects to and end up parsed via `CMessage::InitTime`. A malicious or buggy IRC server can send any year in the tag value. Production impact: arm64/x86 just wrap the multiply, so `tv_sec` becomes garbage and the message ends up with a wrong timestamp in buffer playback. No crash, no memory corruption. UBSan / `-ftrapv` builds: hard abort. Concretely this fires under CIFuzz on any PR that touches the time-parsing path. ## Reproduction ```cpp // poc_servertime.cpp #include <cstdio> #include <znc/Utils.h> int main() { timeval tv = CUtils::ParseServerTime("202620260-5-25T08:00:00Z"); fprintf(stderr, "tv_sec=%lld tv_usec=%ld\n", (long long)tv.tv_sec, (long)tv.tv_usec); } ``` Build: ```bash clang++ -std=c++17 -fsanitize=address,undefined -g -O1 \ -I include -I build/include -include znc/zncconfig.h \ poc_servertime.cpp build/src/libznc.a \ -lpthread -ldl -framework CoreFoundation -L/opt/homebrew/lib -largon2 \ -o poc_servertime UBSAN_OPTIONS=halt_on_error=0:print_stacktrace=1 ./poc_servertime ``` Output: ``` __chrono/duration.h:93:79: runtime error: signed integer overflow: 6394015664380800 * 1000000 cannot be represented in type 'long long' #0 ... chrono::__duration_cast::operator() #3 cctz::detail::join_seconds<long long, 1000000> #4 cctz::parse<...> #5 CUtils::ParseServerTime Utils.cpp:572 #6 main tv_sec=-7004529196414 tv_usec=-410752 ``` ## Fix Validate the seconds range before the cast (or catch it after). Simplest: ```cpp timeval CUtils::ParseServerTime(const CString& sTime) { using namespace std::chrono; system_clock::time_point tp; if (!cctz::parse("%Y-%m-%dT%H:%M:%E*SZ", sTime, cctz::utc_time_zone(), &tp)) return {}; auto secs = duration_cast<seconds>(tp.time_since_epoch()).count(); constexpr int64_t kMaxSec = INT64_MAX / 1'000'000; if (secs > kMaxSec || secs < -kMaxSec) return {}; ... } ``` Could send a PR with the bound check + a regression test. <!-- github-sync-id:4326793874 -->
adam closed this issue 2026-10-05 05:02:37 +02:00
Author
Owner

Same as #2008 ?

Same as #2008 ? <!-- github-comment-id:4318448965 -->
Author
Owner

Yes, I duplicatedly created them by mistake... Sorry for that

Yes, I duplicatedly created them by mistake... Sorry for that <!-- github-comment-id:4318576171 -->
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: adam/znc#1086