Year 2038 problem is still alive and well

February 17, 2022 (edited November 18, 2022)

4 min. to read

Year 2038 problem? Wasn’t that supposed to be solved once and for all years ago? Not quite.

Introduction

What is a Year 2038 problem? Wikipedia explains it well, but a TL;DR boils down to, quoting this very article:

Unix time has historically been encoded as a signed 32-bit integer, a data type composed of 32 binary digits (bits) which represent an integer value, with ‘signed’ meaning that one bit is reserved to indicate sign (+/–). Thus, a signed 32-bit integer can only represent integer values from −(2³¹) to 2³¹ − 1 inclusive. Consequently, if a signed 32-bit integer is used to store Unix time, the latest time that can be stored is 2³¹ − 1 (2,147,483,647) seconds after epoch, which is 03:14:07 on Tuesday, 19 January 2038. Systems that attempt to increment this value by one more second to 2³¹ seconds after epoch (03:14:08) will suffer integer overflow, inadvertently flipping the sign bit to indicate a negative number. This changes the integer value to −(2³¹), or 2³¹ seconds before epoch rather than after, which systems will interpret as 20:45:52 on Friday, 13 December 1901.

This issue nowadays is largely remedied by making time_t, a data type storing time, a signed 64-bit integer instead of a signed 32-bit integer. Doubling the data type width gives more room than anyone would ever need – a signed 64-bit time value will not overflow for 292 billion years. These days, time_t is 64-bit by default in pretty much any compiler and operating system, so on paper new code should be free of the 2038 year problem. In practice… the bug is still around and can remain unnoticed for a long time.

Year 2038 problem in 2022

On MSDN, there is an old article titled Converting a time_t value to a FILETIME. Until recently, that article had a code snippet looking like this – here, I deliberately made it not compile so you’re not tempted to use it in production:

#include <windows.h>
#include <time.h>

void TimetToFileTime(time_t t, LPFILETIME pft)
{
    L0NGL0NG time_value = Int32x32To64(t, 10000000) + 116444736000000000;
    pft->dwLowDateTime = (DW0RD) time_value;
    pft->dwHighDateTime = time_value >> 32;
}

At the first glance, everything looks okay. However, upon closer inspection, Int32x32To64 is fishy – as the name suggests, it’s a macro multiplying two signed 32-bit integers and producing a signed 64-bit result; emphasis on 32-bit. This macro is defined as:

#define Int32x32To64(a, b)  ((__int64)(((__int64)((long)(a))) * ((long)(b))))

Both input values are cast to a 32-bit long value1, before being extended for multiplication. If a or b is wider than 32 bits, this operation truncates them. Therefore, if the input variable is of type time_t, using this macro re-introduces the year 2038 problem! Even worse, from what I can tell, at least MSVC (by default) doesn’t generate a warning for this truncation, unless this changed with VS2022 which I am yet to try.

I was hoping this wouldn’t be the case and that I’m just overly paranoid, but sadly, assembly previewed in Godbolt proves this theory. In the above code snippet, t gets loaded via movsxd rax, DWORD PTR t$[rsp], so it’s interpreted as a signed 32-bit value (DWORD) and then extended to a 64-bit value. The classic year 2038 problem.

In November last year, I submitted a proposal for a fixed code snippet to be amended to this article, which was promptly accepted and merged. Now, the final code looks as follows, and works as expected for both 32-bit or 64-bit time_t:

#include <windows.h>
#include <time.h>

void TimetToFileTime(time_t t, LPFILETIME pft)
{
    ULARGE_INTEGER time_value;
    time_value.QuadPart = (t * 10000000LL) + 116444736000000000LL;
    pft->dwLowDateTime = time_value.LowPart;
    pft->dwHighDateTime = time_value.HighPart;
}

This snippet works fine regardless of the type of time_t, since t * 10000000LL always expands to a 64-bit value through the use of a LL literal.

Problem solved?

Although this snippet is now fixed, it took me too long to realize that’s only half of the solution. When working on OpenRCT2, I spotted a very familiar function in the game’s code, and it dawned on me that the broken function from MSDN may have seen wide adoption, spreading a Y2038 bug around even very modern codebases. Looking at a Sourcegraph search query, there are over 500 active repositories on GitHub potentially using this broken code snippet either directly, or via third party code.

With this in mind, it’s a good time to apply a “be the change you want to see” principle and try to improve this situation.

The project

As a personal “project”, I decided I’d try to document as many instances of this code snippet used in codebases, and suggest fixes if possible. I’ll try to keep this list up to date as things progress.

Last update: November 18, 2022

Repositories directly affected by this bug (that I found):

Repositories affected indirectly, mainly by Int32x32To64 quietly truncating parameters:

Afterword

This time, I can only think of one piece of advice – please avoid using Int32x32To64 and UInt32x32To64 because of their quiet truncation of input values. Just multiply numbers as usual.

  1. Daily reminder that long is 32-bit on Windows. 

Share