C and C++
In brief: C is where the Year 2038 problem started. The C type
time_twas a signed 32-bit number on most 32-bit systems, and it runs out on 19 January 2038. Modern 64-bit systems already use a 64-bittime_t, and 32-bit Linux can now opt in to one. Real code still breaks in three ways: values get squeezed back into anint, they get printed with the wrong format, or two libraries disagree about how bigtime_tis.
| Risk level | High (Critical for 32-bit embedded Linux and long-lived C libraries) |
| Status | 64-bit time_t is the default on 64-bit platforms, musl ≥ 1.2.0 and MSVC. glibc ≥ 2.34 offers it on 32-bit as an opt-in (_TIME_BITS=64). Application code and library ABIs still need auditing |
| Last reviewed | 2026-10-21 |
2038 status at a glance
- At risk ConfirmedApplication code that stores Unix time in a 32-bit integer (int, int32, DWORD, 32-bit long)This is the most common pattern in every language. A 64-bit OS doesn't help if the program truncates time to 32 bits, and it wraps silently.Do: Search code for 32-bit casts and fields around time values, and widen them to 64-bit or native time types.
Overview
ISO C only says that time_t is a "real type capable of representing times". POSIX adds that it counts seconds since 1970-01-01 00:00:00 UTC. Neither standard fixes its width, so the width depends on the platform:
- 64-bit Unix-like systems (LP64):
time_tislong, which is 64 bits. These systems are safe for roughly 292 billion years. - 32-bit Linux with glibc:
time_tis still 32 bits by default. glibc 2.34 (released 2021-08-01) added a 64-bittime_tthat a program enables at compile time with_TIME_BITS=64. glibc only allows it together with_FILE_OFFSET_BITS=64, and full support needs Linux 5.1 or later [1]. - 32-bit Linux with musl: musl 1.2.0 made
time_t64-bit on all architectures. It kept old binaries working by redirecting the affected functions to new symbol names such as__gmtime64_r[3]. - Windows (MSVC/UCRT):
time_tis__time64_tby default, which "supports dates through 23:59:59, December 31, 3000 UTC". Defining_USE_32BIT_TIME_Tbrings back the 32-bit type. Microsoft warns that "your application may fail after January 18, 2038", and the macro isn't allowed on 64-bit targets [5].
A fixed C library does not make an application safe. The bugs that remain sit in application code and in the interfaces between separately compiled pieces.
Technical details
1. Turning on 64-bit time on 32-bit glibc
# Both macros are required; glibc errors out if only _TIME_BITS is set.
gcc -D_FILE_OFFSET_BITS=64 -D_TIME_BITS=64 -o app app.c
If _TIME_BITS=64 is set without LFS, the build stops with #error "_TIME_BITS=64 is allowed only with _FILE_OFFSET_BITS=64". PulseAudio's padsp.c hit exactly this error because the file forced _FILE_OFFSET_BITS to be undefined [9]. Autoconf projects that use Gnulib's year2038 module get _TIME_BITS=64 by default on 32-bit glibc ≥ 2.34. The stricter year2038-recommended module "by default rejects platforms where time_t cannot represent timestamps after 2038". --disable-year2038 is the escape hatch for linking against libraries "whose user-facing ABIs still require time_t to be 32-bit" [2][10].
2. Truncating to int (the most common bug)
#include <stdio.h>
#include <stdint.h>
#include <time.h>
int main(void) {
time_t t = (time_t)INT32_MAX + 1; /* 2038-01-19 03:14:08 UTC */
int bad = (int)t; /* BUG: wraps to -2147483648 (1901) */
uint32_t wire = (uint32_t)t; /* "works" until 2106, then wraps */
printf("bad = %d\n", bad);
printf("good = %lld\n", (long long)t); /* FIX: keep 64 bits */
return 0;
}
On x86-64 Linux (GCC, glibc, 64-bit time_t) this prints bad = -2147483648 and good = 2147483648 (tested 2026-10-21). The same truncation hides in struct fields (int expires;), function parameters (void set_timeout(int when)), long on 32-bit or Windows targets (where long is 32 bits even with a 64-bit time_t), arithmetic such as int delta = t1 - t0;, and serialisation (htonl(t)).
Fix: keep time_t (or int64_t) end to end. When a 32-bit wire field can't be changed, convert explicitly and range-check, or switch the field to unsigned 32-bit with a documented epoch. That buys time until 2106 but doesn't solve the problem.
3. printf/scanf format specifiers
There is no standard printf conversion for time_t. %ld happens to work on LP64 but is undefined behaviour on 32-bit targets once time_t becomes 64-bit. musl's time64 notes list "use %ld for time_t" as a known breakage [3].
printf("%ld\n", t); /* BUG on 32-bit + _TIME_BITS=64 */
printf("%jd\n", (intmax_t)t); /* FIX: portable */
printf("%lld\n", (long long)t); /* FIX: also fine */
printf("%" PRId64 "\n", (int64_t)t); /* FIX: with <inttypes.h> */
struct timespec.tv_nsec is a long, so print it with %ld. Only tv_sec is a time_t.
4. struct timespec, struct timeval and friends
Every structure that contains a time_t changes size and layout when time_t grows: struct timespec, struct timeval, struct stat (st_mtim), struct utimbuf, struct itimerspec, struct input_event and others. On 32-bit glibc, struct timespec with _TIME_BITS=64 has a 64-bit tv_sec and padding around the 32-bit tv_nsec. Code that hand-builds these structures, declares time functions itself instead of including the headers, or issues raw syscall() calls with them has undefined behaviour after the switch. musl names direct syscalls, seccomp filters and syscall tracers as things that may need updating [3].
5. ABI breakage for libraries that expose time_t
When a shared library has time_t, or a struct containing one, in its public API, the library and every caller must agree on time_t's width. musl puts it this way: "an ABI mismatch will arise if they were built with mismatching definitions of time_t" [3]. Because of this:
- Debian renamed every affected library on 32-bit Arm with a
t64suffix (for examplelibfoo1→libfoo1t64) and rebuilt roughly 4,900–6,200 source packages. The i386 port was deliberately left on 32-bit time to keep running legacy binaries [4]. See OS-Linux. - OpenJDK on Ubuntu's armhf port used
dlsym("futimens")and got the 32-bit compatibility symbol, so file times came back as 1976 instead of 2017 (JDK-8336529) [11]. Looking up libc symbols by name skips the header redirection that_TIME_BITS=64relies on. - .NET's Arm32 runtime has to detect at run time whether the system OpenSSL was built with a 32-bit or 64-bit
time_t, and it "avoid[s] passing time values that don't fit in 32 bits to openssl" [12].
Library maintainers should treat a time_t width change as an SONAME bump. Alternatively they can add explicit int64_t entry points (foo_set_expiry64()) and keep the old ones as thin wrappers.
6. C++ std::chrono
Since C++20, std::chrono::system_clock is specified to measure Unix time [6]. Its rep is implementation-defined. Mainstream implementations use a 64-bit count of nanoseconds, which gives about ±292 years around 1970: safe for 2038, but not unlimited [7]. The usual C++ bug is converting at the C boundary:
#include <chrono>
using namespace std::chrono;
auto now = system_clock::now();
int bad = static_cast<int>(system_clock::to_time_t(now)); // BUG
auto ok = duration_cast<seconds>(now.time_since_epoch()).count(); // FIX: int64
sys_seconds s{seconds{ok}}; // C++20 round-trip
std::chrono::seconds uses a signed integer of at least 35 bits, so it's safe. The truncation happens in user code and in to_time_t on platforms where time_t is still 32-bit.
Affected versions / products
| Environment | Default time_t |
64-bit option | Notes |
|---|---|---|---|
| glibc on 64-bit Linux | 64-bit | n/a | Safe |
| glibc ≥ 2.34 on 32-bit Linux | 32-bit | -D_TIME_BITS=64 -D_FILE_OFFSET_BITS=64 |
Kernel ≥ 5.1 needed for full support [1] |
| glibc < 2.34 on 32-bit Linux | 32-bit | none | Upgrade glibc |
| musl ≥ 1.2.0 | 64-bit everywhere | default | Old binaries keep working via symbol redirects [3] |
| MSVC / UCRT | 64-bit (__time64_t) |
default | _USE_32BIT_TIME_T is unsafe and not allowed on 64-bit [5] |
| Debian armhf (trixie onward) | 64-bit via dpkg-buildflags |
t64 library transition | i386 stays 32-bit by design [4] |
Risk & compliance notes
- Inventory: every 32-bit build target, including cross-compiled firmware; every shared library whose public headers mention
time_t,timespec,timevalorstat; every on-disk or on-wire struct thatmemcpys a time value. - Ownership: platform/BSP teams own the toolchain flags. Library owners own ABI versioning. Application teams own truncation bugs.
- Hidden early failures: certificate expiry dates, licence keys, "valid until" fields and 30-year mortgages hit post-2038 dates today, not in 2038.
Detection & mitigation
- Compiler warnings.
-Wall -Wextradoes not warn aboutint x = time(NULL);.-Wconversiondoes: GCC reportsconversion from 'time_t' {aka 'long int'} to 'int' may change value(verified with GCC on Debian, 2026-10-21). Clang has-Wshorten-64-to-32. An explicit cast such as(int)tsilences both, so grep for casts as well. - Format checking. Build the 32-bit target with
-Wformatand_TIME_BITS=64.%ldwith a 64-bittime_tthen warns. - Static analysis.
clang-tidy'sbugprone-narrowing-conversionscatches implicit narrowing but skips explicit casts. A June 2026 LLVM RFC proposes a dedicated Y2038 check for lossytime_tconversions, including(int)time(NULL)anduint32_t u = t;. As of 2026-10-21 it is a prototype, not a shipped check [8]. - Grep patterns:
\(int\)\s*time,(int32_t|uint32_t|long)\s+\w*(time|ts|expir|stamp),htonl\(.*time,%ld.*tv_sec. - Runtime test: run the test suite on a 32-bit build with the clock set past 2038. See Testing-and-Detection.
- Remediate with the steps in Remediation-Guide.
See also
- Programming-Languages (matrix of all languages)
- OS-Linux, OS-Embedded-and-RTOS, OS-Windows
- Lang-Go-Rust-Swift (Rust
libccrate and Gosyscallon 32-bit) - File-Formats-and-Protocols
- Year-2038-Problem
References
- GNU C Library 2.34 released — LWN.net (release announcement), 2021-08-01 — 64-bit
time_ton 32-bit configurations;_TIME_BITS=64requires_FILE_OFFSET_BITS=64; Linux only; kernel ≥ 5.1. - Avoiding the year 2038 problem — Gnulib manual, GNU —
year2038vsyear2038-recommended,--disable-year2038, default_TIME_BITS=64on glibc ≥ 2.34. - time64 — musl libc — musl 1.2.0 64-bit
time_t, symbol redirection, ABI-mismatch warning,%ldand syscall caveats. - Debian's plan for 64-bit time_t — LWN.net (Jonathan Corbet), 2023-07-17 — t64 library renames, 4,900–6,200 source packages, i386 excluded.
- time, _time32, _time64 — Microsoft Learn —
time_tis__time64_tby default (to year 3000);_USE_32BIT_TIME_Twarning. - std::chrono::system_clock — cppreference.com — Unix-time epoch since C++20;
repimplementation-defined. - std-proposals mailing list post, 2019-06-26 — ISO C++ — common
system_clockuses int64 nanoseconds, giving ±292 years. - RFC: Y2038 check for lossy time_t conversions — LLVM Discourse, 2026-06 — proposed clang-tidy check; limits of
bugprone-narrowing-conversionswith explicit casts. - padsp.c: does not build with _TIME_BITS=64 — PulseAudio issue #3770, freedesktop GitLab — real-world
_TIME_BITS/LFS build failure. - year2038: support glibc 2.34 _TIME_BITS=64 — Paul Eggert, bug-gnulib, 2021-07-02 — apps must define both macros.
- RFR: 8336529: (fs) UnixFileAttributeViews setTimes() failing on armhf, Ubuntu noble — OpenJDK nio-dev, 2024-07 —
dlsymreturning 32-bit time symbols after the Debian/Ubuntu transition. - Build against Y2038-compatible glibc for linux arm32 — dotnet/runtime PR #102410, merged 2024-05-22 — runtime detection of OpenSSL
time_twidth.