~ / topics / Languages
$man lang-c-and-cpp

C and C++

In brief: C is where the Year 2038 problem started. The C type time_t was 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-bit time_t, and 32-bit Linux can now opt in to one. Real code still breaks in three ways: values get squeezed back into an int, they get printed with the wrong format, or two libraries disagree about how big time_t is.

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

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:

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:

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

Detection & mitigation

  1. Compiler warnings. -Wall -Wextra does not warn about int x = time(NULL);. -Wconversion does: GCC reports conversion 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)t silences both, so grep for casts as well.
  2. Format checking. Build the 32-bit target with -Wformat and _TIME_BITS=64. %ld with a 64-bit time_t then warns.
  3. Static analysis. clang-tidy's bugprone-narrowing-conversions catches implicit narrowing but skips explicit casts. A June 2026 LLVM RFC proposes a dedicated Y2038 check for lossy time_t conversions, including (int)time(NULL) and uint32_t u = t;. As of 2026-10-21 it is a prototype, not a shipped check [8].
  4. Grep patterns: \(int\)\s*time, (int32_t|uint32_t|long)\s+\w*(time|ts|expir|stamp), htonl\(.*time, %ld.*tv_sec.
  5. Runtime test: run the test suite on a 32-bit build with the clock set past 2038. See Testing-and-Detection.
  6. Remediate with the steps in Remediation-Guide.

See also

References

  1. GNU C Library 2.34 released — LWN.net (release announcement), 2021-08-01 — 64-bit time_t on 32-bit configurations; _TIME_BITS=64 requires _FILE_OFFSET_BITS=64; Linux only; kernel ≥ 5.1.
  2. Avoiding the year 2038 problem — Gnulib manual, GNU — year2038 vs year2038-recommended, --disable-year2038, default _TIME_BITS=64 on glibc ≥ 2.34.
  3. time64 — musl libc — musl 1.2.0 64-bit time_t, symbol redirection, ABI-mismatch warning, %ld and syscall caveats.
  4. 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.
  5. time, _time32, _time64 — Microsoft Learn — time_t is __time64_t by default (to year 3000); _USE_32BIT_TIME_T warning.
  6. std::chrono::system_clock — cppreference.com — Unix-time epoch since C++20; rep implementation-defined.
  7. std-proposals mailing list post, 2019-06-26 — ISO C++ — common system_clock uses int64 nanoseconds, giving ±292 years.
  8. RFC: Y2038 check for lossy time_t conversions — LLVM Discourse, 2026-06 — proposed clang-tidy check; limits of bugprone-narrowing-conversions with explicit casts.
  9. padsp.c: does not build with _TIME_BITS=64 — PulseAudio issue #3770, freedesktop GitLab — real-world _TIME_BITS/LFS build failure.
  10. year2038: support glibc 2.34 _TIME_BITS=64 — Paul Eggert, bug-gnulib, 2021-07-02 — apps must define both macros.
  11. RFR: 8336529: (fs) UnixFileAttributeViews setTimes() failing on armhf, Ubuntu noble — OpenJDK nio-dev, 2024-07 — dlsym returning 32-bit time symbols after the Debian/Ubuntu transition.
  12. Build against Y2038-compatible glibc for linux arm32 — dotnet/runtime PR #102410, merged 2024-05-22 — runtime detection of OpenSSL time_t width.