epochkit
unix security systems

The Year 2038 Problem

On January 19, 2038, 32-bit Unix timestamps overflow. Explains why it happens, which systems are at risk, the state of 64-bit migration, and what developers should do today.

Share X
Table of contents

TL;DR: A signed 32-bit integer can hold a maximum value of 2,147,483,647. That value corresponds to January 19, 2038 at 03:14:07 UTC as a Unix timestamp. Any software that stores Unix time in a 32-bit int will roll over to a negative number at that moment — equivalent to December 13, 1901. Modern 64-bit systems are safe, but embedded systems, databases, and legacy code may not be.


What breaks at 03:14:07 UTC on January 19, 2038

Unix timestamps count seconds since the Unix epoch (January 1, 1970 00:00:00 UTC). For the past 50+ years, they have been stored in signed 32-bit integers on most systems.

At exactly 03:14:08 UTC on January 19, 2038, a signed 32-bit counter overflows:

Max signed 32-bit value: 2,147,483,647
Next value:             -2,147,483,648  ← overflow
Interpreted as date:     December 13, 1901 at 20:45:52 UTC

Software that reads a timestamp stored in a 32-bit field will suddenly see a date 136 years in the past. Depending on what the software does with that value, the consequences range from cosmetic display errors to complete failure — expired certificates, invalid file timestamps, broken scheduling, and security vulnerabilities.


Why 32-bit signed integers cannot hold it

A 32-bit signed integer uses one bit for the sign, leaving 31 bits for the value:

2^31 - 1 = 2,147,483,647 seconds from the epoch
         = approximately 68 years
         = January 19, 2038 at 03:14:07 UTC

The fix is obvious in hindsight: use a 64-bit integer instead. A signed 64-bit counter can represent times up to approximately the year 292,277,026,596 — safely beyond any practical concern.

The difficulty is not fixing new code; it is finding every place where 32-bit timestamps are already stored, computed, or transmitted.


Status of the 64-bit migration

Linux kernel

The Linux kernel on 64-bit platforms has used 64-bit time_t since the late 1990s. The critical work was the 32-bit ARM and x86 userspace migration. Since kernel 5.6 (released March 2020), the kernel explicitly supports 32-bit systems with a 64-bit time_t through a new set of syscalls (clock_gettime64, clock_settime64, etc.).

However, userspace programs compiled against older libc versions on 32-bit systems still use 32-bit time_t. A system must recompile against a modern libc (glibc 2.32+ or musl) to be fully safe.

glibc and musl

glibc changed time_t to 64-bit on 32-bit architectures in version 2.34 (August 2021), but this requires rebuilding all affected packages. Most Linux distributions are working through this.

musl libc has used 64-bit time_t on all platforms since version 1.2.0 (February 2020). Alpine Linux, which uses musl, is already safe.

Filesystems

ext4 stores timestamps in 34-bit fields (32 bits plus 2 extra bits for range extension), giving it a range through the year 2446. Safe.

XFS historically used 32-bit timestamps. Version 5 (the default since Red Hat Enterprise Linux 8) uses 64-bit timestamps. Systems running XFS v4 are at risk.

FAT32 and older filesystems store dates in their own format (year since 1980, 7 bits), which limits them to 2107 regardless — a separate but related problem.

Embedded and IoT systems

This is the highest-risk category. Many embedded systems — industrial controllers, networking equipment, medical devices, automotive systems — run on 32-bit processors with firmware built against old toolchains and libraries. They may not be updatable at all, and many have a lifespan that extends past 2038.

Real-Time Clocks (RTCs) on microcontrollers often store time in BCD (binary-coded decimal) or a small integer field. Some only represent the year as two digits or a small offset.


How to audit your codebase

Search for 32-bit time types:

# C/C++: look for time_t and 32-bit integer types used for time
grep -rn --include="*.c" --include="*.h" \
  -e 'time_t' -e 'int32_t' -e '__int32' \
  src/

# Check sizeof(time_t) on your target platform
cat > /tmp/check_time.c << 'EOF'
#include <stdio.h>
#include <time.h>
int main() {
  printf("sizeof(time_t) = %zu\n", sizeof(time_t));
  return 0;
}
EOF
gcc /tmp/check_time.c -o /tmp/check_time && /tmp/check_time
# Safe output: sizeof(time_t) = 8
# Unsafe output: sizeof(time_t) = 4

Check database column types:

-- PostgreSQL: find TIMESTAMP columns (32-bit risk only in very old versions)
SELECT table_name, column_name, data_type
FROM information_schema.columns
WHERE data_type IN ('timestamp', 'timestamp without time zone')
AND table_schema = 'public';

-- MySQL: check INT columns that might store Unix time
-- Look for columns named *_at, *_time, *_date stored as INT or INT(11)
SELECT table_name, column_name, column_type
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND column_type IN ('int', 'int(11)', 'int unsigned')
AND (column_name LIKE '%_at' OR column_name LIKE '%_time' OR column_name LIKE '%_date');

Check file format definitions: Any binary format that reserves 4 bytes for a Unix timestamp is a candidate. Look for struct tm, uint32_t time, or similar patterns in protocol definitions.


What to do today

For new code: Always use 64-bit integers or your language’s native datetime type for time storage. Never use int or int32 for Unix timestamps.

// C: use int64_t, not int or int32_t
#include <stdint.h>
int64_t get_unix_seconds(void) {
    struct timespec ts;
    clock_gettime(CLOCK_REALTIME, &ts);
    return (int64_t)ts.tv_sec;
}
# Python: time.time() returns a float; int(time.time()) is safe on 64-bit Python
import time
sec: int = int(time.time())  # safe — Python ints are arbitrary precision
// Go: time.Unix() returns time.Time; Unix() method returns int64
t := time.Now().Unix()  // int64, always safe

For database schemas: Migrate INT or INT(11) timestamp columns to BIGINT in MySQL, or use the native TIMESTAMP type in PostgreSQL (which uses 64-bit internally since v7.3).

-- MySQL: migrate a 32-bit unix timestamp column to 64-bit
ALTER TABLE events MODIFY COLUMN created_at BIGINT NOT NULL;

For embedded systems: Audit firmware and third-party libraries for time_t size. If the toolchain does not support 64-bit time_t, consider an offset epoch (e.g., January 1, 2000) to push the overflow further out, or store time as separate year/month/day/hour/minute/second fields.

For legacy systems you cannot upgrade: Document the risk, set a calendar reminder for January 2037 to revisit, and monitor the vendor’s advisory for patches.


To check any Unix timestamp value and see what date it represents, use epochkit’s Unix Timestamp Converter. Enter a timestamp close to 2147483647 to see exactly when the 32-bit overflow would occur for your system.