~ / topics / Testing & Remediation
$man testing-and-detection

Testing and Detection

In brief: You cannot fix a Year 2038 bug you haven't found. This page shows engineers how to find these bugs in four ways: run software under a fake future clock, scan source code and compiled binaries for 32-bit time handling, audit database schemas, and run small probe programs. Every technique here is meant to run in a disposable test environment, never in production.

Risk level n/a (methodology page)
Status Tooling is mature for Linux/glibc; static analysis for time_t truncation is still partial as of 2026-10-21
Last reviewed 2026-10-21

Overview

A Year 2038 defect appears whenever a time value passes through something only 32 bits wide: a time_t on an old ABI, an int column, a uint32_t struct field, a %d format string or a binary file header. Testing therefore comes in two kinds:

  1. Dynamic testing, where the software runs while it believes it is 2038 or later and you watch what breaks.
  2. Static detection, where you look for the narrow types directly in source, binaries and schemas.

Neither kind is enough by itself. A fake clock only exercises the code paths you actually run. Static scans produce false positives and miss timestamps hidden in opaque blobs. Use both.

Safety first. Kaspersky's 2026 guidance for organisations says never to test on production systems. It also says to back up data first, isolate the system under test, keep forged NTP or GPS time away from other systems, reset the clock afterwards and document everything [16]. A clock jump on a shared host can expire certificates, Kerberos tickets, licences and log-retention jobs on every machine that trusts it.

Technical details

1. Faking the clock for one process: libfaketime

libfaketime intercepts libc time calls through LD_PRELOAD. Its README lists "testing software for year-2038 compliance" as a use case [1].

# Debian/Ubuntu
sudo apt install faketime libfaketime

# Absolute start time (clock keeps ticking from here)
faketime '2038-01-19 03:13:00' date
FAKETIME='@2038-01-19 03:13:00' LD_PRELOAD=/usr/lib/x86_64-linux-gnu/faketime/libfaketime.so.1 ./myapp

# Relative offset: 12 years ahead, running at 10x speed
FAKETIME='+12y x10' LD_PRELOAD=.../libfaketime.so.1 ./myapp

# JVM: avoid hangs by not faking the monotonic clock
FAKETIME_DONT_FAKE_MONOTONIC=1 faketime '2038-01-19 03:14:00' java -jar app.jar

The README documents "YYYY-MM-DD hh:mm:ss", the @ start-at prefix, +/- offsets and x speed multipliers, and FAKETIME_DONT_FAKE_MONOTONIC for Java [1]. The library path differs between distributions, so check it with dpkg -L libfaketime.

Limitations [1]:

If a Go service ignores FAKETIME, move up to a VM.

2. Changing the clock of a whole OS: date -s, VMs and containers

# Inside a disposable VM only. Stop NTP first or it will undo your change.
sudo timedatectl set-ntp false
sudo date -u -s '2038-01-19 03:13:30'
sleep 60; date -u          # watch the rollover
journalctl -b --since '-5 min' | grep -iE 'error|overflow|invalid|1901|1970'

Containers don't get their own wall clock. Linux time namespaces were added in Linux 5.6 (released 2020-03-29) [3]. They virtualise only CLOCK_MONOTONIC and CLOCK_BOOTTIME, not CLOCK_REALTIME [2]. The kernel authors cited the complexity and overhead of virtualising the realtime clock [2][4]. As a result:

# Shift only the boot-time and monotonic clocks for child processes (needs CONFIG_TIME_NS)
sudo unshare -T -- bash --norc -c '
  echo "boottime 604800 0"   > /proc/$$/timens_offsets   # +7 days
  echo "monotonic 172800 0"  > /proc/$$/timens_offsets   # +2 days
  exec bash'

The offset format is <clock-id> <secs> <nanosecs>, and writing it requires CAP_SYS_TIME [2]. For calendar dates inside containers, use libfaketime in the container image or run a full VM.

3. QEMU: boot a guest whose RTC starts in the future

QEMU's -rtc base= accepts utc, localtime or a datetime such as 2006-06-17T16:01:21 [6].

# x86 guest starting just before the rollover, isolated from host NTP
qemu-system-x86_64 -m 2G -enable-kvm -drive file=test.qcow2,if=virtio \
  -rtc base=2038-01-19T03:10:00,clock=vm -nic none

# 32-bit ARM board emulation (the class most at risk)
qemu-system-arm -M virt -m 1G -kernel zImage -initrd rootfs.cpio.gz \
  -append 'console=ttyAMA0' -nographic -rtc base=2037-12-31T23:50:00

The -nic none option keeps the guest from reaching NTP. clock=vm decouples guest time from the host [6]. QEMU's own RTC test sets a date in 2080 "to ensure there is no year-2038 overflow". That test caught a real overflow in mktimegm (see Known-2038-Incidents).

4. Probe program: what does this toolchain actually do?

Compile this probe for each target ABI. On 32-bit glibc it reports truthfully only when built with -D_TIME_BITS=64 -D_FILE_OFFSET_BITS=64, which glibc 2.34+ supports on Linux ≥ 5.1 [7].

/* y2038-probe.c */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdint.h>
#include <time.h>
int main(void) {
    printf("sizeof(time_t) = %zu\n", sizeof(time_t));
    int64_t big = INT64_C(2147483648);            /* 2038-01-19 03:14:08 UTC */
    time_t t = (time_t)big;
    if ((int64_t)t != big) { printf("FAIL: truncated to %lld\n", (long long)t); return 1; }
    struct tm tm; char buf[64];
    if (!gmtime_r(&t, &tm)) { puts("FAIL: gmtime_r"); return 1; }
    strftime(buf, sizeof buf, "%Y-%m-%d %H:%M:%S", &tm);
    printf("gmtime(2^31) = %s UTC\n", buf);
    printf("timegm round-trip %s\n", timegm(&tm) == t ? "OK" : "FAIL");
    return 0;
}
arm-linux-gnueabihf-gcc -o probe32 y2038-probe.c                                   # legacy ABI
arm-linux-gnueabihf-gcc -D_TIME_BITS=64 -D_FILE_OFFSET_BITS=64 -o probe64 y2038-probe.c
qemu-arm -L /usr/arm-linux-gnueabihf ./probe32; qemu-arm -L /usr/arm-linux-gnueabihf ./probe64

The listing above was verified to compile and pass on x86-64 glibc 2.41 on 2026-10-21. The ARM lines need a cross toolchain and were not run.

5. Grep patterns for narrow timestamps in source

Use PCRE (grep -P, rg -P or ugrep -P). These patterns are heuristics: review every hit by hand.

# C/C++: 32-bit integer variables whose names suggest time
grep -rnP --include='*.[ch]' --include='*.cpp' --include='*.cc' \
 '\b(?:int|long|int32_t|uint32_t|unsigned(?:\s+int)?|INT32|DWORD)\s+\**\s*\w*(?:time|_ts\b|stamp|expir|created|updated|deadline|last_?seen)\w*' .

# Casts that truncate time values
grep -rnP '\((?:int|long|int32_t|uint32_t|unsigned)\)\s*(?:time\s*\(|[\w.>-]*(?:tv_sec|st_[amc]time))' .

# printf of time values with %d / %ld (wrong on 32-bit, hides truncation)
grep -rnP '%l?d[^"]*"[^;]*\b(?:time\s*\(|tv_sec|st_mtime)' .

# Python / NumPy packing into 32 bits
grep -rnP 'struct\.pack\([^)]*["'"'"'][<>!=@]?[iIlL]["'"'"'][^)]*time|np\.u?int32\([^)]*time' .

# Java: int seconds, (int)(System.currentTimeMillis()/1000)
grep -rnP '\(int\)\s*\(?\s*System\.currentTimeMillis\(\)\s*/\s*1000' --include='*.java' .

# SQL DDL: integer or TIMESTAMP columns that look like times
grep -rniP '\b\w*(?:time|_at|stamp|date|expir)\w*\s+(?:int|integer|int\(\d+\)|mediumint|timestamp)\b' --include='*.sql' .

# Sentinel "forever" values that overflow when added to now (AOLserver-style)
grep -rnP '\b(?:1000000000|2147483647|0x7fffffff|INT_MAX|INT32_MAX)\b' .

Each pattern was tested against a small fixture on 2026-10-21. Expect many false positives from int loop counters named timeout, for example.

6. Static analysers

Tool What it finds How to run
cppcheck y2038 addon Errors if _TIME_BITS or _FILE_OFFSET_BITS ≠ 64. Warns on Y2038-unsafe identifiers such as timeval, localtime, gettimeofday and stat [9] cppcheck --addon=y2038 src/
Coverity Y2K38_SAFETY time_t stored into a narrower type. It has reported real findings in BIND 9 and Samba, plus some false alarms [12] Commercial; enabled in Coverity Scan
clang-tidy No dedicated Y2038 check is merged. A June 2026 RFC proposes portability-y2038-lossy or a time_t mode of bugprone-narrowing-conversions [10] Interim: clang-tidy -checks='bugprone-narrowing-conversions,cppcoreguidelines-narrowing-conversions'
y2k38-checker Clang Static Analyzer plugin. Flags time_t→int/long downcasts and st_*time reads and writes [11] clang -fplugin=liby2k38-plugin.so ... (LLVM 11)
GCC warnings Implicit narrowing in general -Wconversion -Wsign-conversion -Wformat=2
Gnulib year2038-recommended Makes configure fail where time_t cannot pass 2038 [15] Add the module in bootstrap.conf

Note: as of 2026-10-21, the cppcheck addon's identifier list has a missing comma after "time_t". Python concatenates the two literals, so the addon never flags a bare time_t [9]. Rely on its _TIME_BITS check, not on that identifier.

7. ABI analysis: the Debian approach

Debian's 64-bit time_t transition ran abi-compliance-checker over every -dev package. It compared each library's ABI built with and without -D_TIME_BITS=64 -D_FILE_OFFSET_BITS=64. Of 5,237 packages with shared libraries, 329 changed ABI because of time_t and 58 because of LFS. In 1,637 cases the checker failed or gave an inconclusive result, and Debian treated those as affected [8]. To reproduce this for an in-house library:

# Build two ABI dumps of the same library and compare them
abi-dumper libfoo32.so -o foo-time32.dump -lver time32
abi-dumper libfoo64.so -o foo-time64.dump -lver time64     # built with abi=+time64
abi-compliance-checker -l libfoo -old foo-time32.dump -new foo-time64.dump
# Debian packaging: DEB_BUILD_MAINT_OPTIONS="abi=+time64" dpkg-buildpackage  [8]

8. Binary / ELF scanning for 32-bit time symbols

On 32-bit glibc, the headers redirect time() to __time64, clock_gettime() to __clock_gettime64 and so on when _TIME_BITS=64 is set [7]. A 32-bit ELF that imports the plain names was therefore built with 32-bit time_t. The Yocto Project automates this as its 32bit-time QA check. The check runs objdump -tw and matches undefined symbols against a hand-built list of about 110 glibc APIs [13]. A portable equivalent:

#!/usr/bin/env bash
# scan-time32.sh DIR...  -- flag 32-bit ELF objects importing 32-bit time APIs
set -euo pipefail
API='time|stime|gettimeofday|settimeofday|clock_gettime|clock_settime|clock_getres|clock_nanosleep|nanosleep|gmtime|gmtime_r|localtime|localtime_r|mktime|timegm|ctime|ctime_r|difftime|stat|stat64|fstat|fstat64|lstat|lstat64|fstatat|fstatat64|utime|utimes|futimes|lutimes|utimensat|futimens|select|pselect|ppoll|timer_settime|timer_gettime|timerfd_settime|timerfd_gettime|setitimer|getitimer|pthread_cond_timedwait|pthread_mutex_timedlock|sem_timedwait|mq_timedsend|mq_timedreceive|recvmmsg|getrusage|wait3|wait4|adjtime|adjtimex|clock_adjtime|sigtimedwait|semtimedop|setsockopt|getsockopt|ioctl|fcntl|fcntl64'
find "$@" -type f -size +0 2>/dev/null | while read -r f; do
  [ "$(head -c 4 "$f" 2>/dev/null | od -An -c | tr -d ' ')" = '177ELF' ] || continue
  [ "$(od -An -tu1 -j4 -N1 "$f" | tr -d ' ')" = 1 ] || continue          # ELFCLASS32
  hits=$(nm -D --undefined-only "$f" 2>/dev/null | awk '{print $NF}' | sed 's/@.*//' \
         | grep -Ex "$API" | sort -u | tr '\n' ' ' || true)
  t64=$(nm -D --undefined-only "$f" 2>/dev/null | grep -cE '64[a-z_]*@GLIBC_2\.(3[4-9]|[4-9][0-9])' || true)
  [ -n "$hits" ] && printf 'TIME32  %s  [%s] (time64 imports: %s)\n' "$f" "${hits% }" "$t64"
done

Run against Debian 13's i386 multiarch libraries on 2026-10-21, the script flagged files such as libxml2.so.2 (time, stat64, select). That is expected, because Debian deliberately kept i386 on 32-bit time_t [8]. Notes:

9. Database schema audits

MySQL's TIMESTAMP range ends at 2038-01-19 03:14:07 UTC in every manual version from 5.7 to 9.7 [14]. MariaDB 11.5.1 extended it to 2106 on 64-bit platforms [18].

-- MySQL / MariaDB: TIMESTAMP columns and integer columns that look like epochs
SELECT table_schema, table_name, column_name, column_type
FROM information_schema.columns
WHERE table_schema NOT IN ('mysql','sys','information_schema','performance_schema')
  AND (data_type = 'timestamp'
       OR (data_type IN ('int','mediumint') AND
           column_name REGEXP '(time|_at$|stamp|date|expir|_ts$)'));

-- Find live data already close to the limit
SELECT MAX(expires_at) FROM sessions;               -- repeat per candidate column
SELECT FROM_UNIXTIME(2147483648);                   -- NULL/0 on servers older than 8.0.28 [17]

-- PostgreSQL: 4-byte integers that may hold epoch seconds
SELECT table_schema, table_name, column_name, data_type
FROM information_schema.columns
WHERE data_type = 'integer'
  AND column_name ~* '(time|_at$|stamp|date|expir|_ts$)'
  AND table_schema NOT IN ('pg_catalog','information_schema');
# SQLite: integers have no fixed width, but app code may pack them. Check the schema anyway.
sqlite3 app.db ".schema" | grep -iP '\w*(time|_at|stamp|expir)\w*\s+int'

Also check ORM models (IntegerField, @Column(columnDefinition="INT")), Protobuf int32/fixed32/sfixed32 fields and Avro int for time fields.

10. A minimal end-to-end test harness

#!/usr/bin/env bash
# y2038-smoke.sh CMD...  -- run a command at several boundary instants and diff behaviour
set -u
for T in '2037-12-31 23:59:00' '2038-01-19 03:13:59' '2038-01-19 03:14:09' \
         '2038-02-01 00:00:00' '2106-02-07 06:28:20'; do
  echo "=== $T ==="
  faketime "$T" "$@" > "out.$(echo "$T" | tr ' :' '__').log" 2>&1
  echo "exit=$?"
  grep -HiE '1901|1970|overflow|invalid|EINVAL|negative' out.*.log | tail -3
done

Pick boundary instants that include just before and after 2^31 seconds, and one past 2^32 seconds (2106) to catch "unsigned" fixes. Also test with future data at today's date: insert a certificate, licence, mortgage end date or retention date beyond 2038 while the clock reads 2026. As Known-2038-Incidents shows, most real failures so far came from future data, not from a future clock.

Affected versions / products

Environment Testable with Caveat
Dynamically linked Linux/macOS user space libfaketime [1] Not static, setuid or Go binaries
Any OS image QEMU -rtc base= [6], VM date -s Disable NTP
Linux containers libfaketime only Time namespaces don't shift CLOCK_REALTIME [2]
32-bit glibc binaries ELF symbol scan [13] Static binaries need symbol tables
C/C++ source cppcheck, Coverity, y2k38-checker [9][11][12] High false-positive rate
MySQL TIMESTAMP Schema query [14] Range fixed at 2038 in MySQL

Risk & compliance notes

Record each test as evidence: the tool, the simulated instants, the result and who signed it off. The Risk-Assessment-Checklist uses these records. If an ELF scan or schema audit finds something in a vendor product, that is a supply-chain finding. Raise it with the vendor rather than patching locally.

Detection & mitigation

When you find a defect, go to the Remediation-Guide, which is organised by layer. Language-specific traps are covered on Lang-C-and-CPP, Lang-Python, Lang-Java-and-JVM and Databases.

See also

References

  1. libfaketime README — wolfcw/libfaketime, GitHub — FAKETIME syntax, Java flag, static/setuid/Go limitations, "year-2038 compliance" use case.
  2. time_namespaces(7) — man7.org — only MONOTONIC/BOOTTIME virtualised; timens_offsets format; unshare -T example.
  3. Linux 5.6 — KernelNewbies, released 2020-03-29 — time namespace added in 5.6.
  4. The Time Namespace Appears To Finally Be On-Deck For The Mainline Linux Kernel — Phoronix, 2020-01-14 — background on why realtime is not namespaced.
  5. systemd-nspawn(1) — man7.org — "the system clock may not be changed from within the container".
  6. QEMU System Emulation: Invocation — qemu.org — -rtc base=…,clock=… syntax and datetime format.
  7. GNU C Library 2.34 released — LWN (libc-alpha announcement), 2021-08-01 — _TIME_BITS=64 requires LFS and Linux ≥ 5.1.
  8. ReleaseGoals/64bit-time — Debian Wiki — abi-compliance-checker statistics, abi=+time64, i386 exclusion.
  9. cppcheck addons/y2038.py — danmar/cppcheck, GitHub — checks, message IDs, unsafe identifier list.
  10. RFC: Y2038 check for lossy time_t conversions — LLVM Discourse, 2026-06-12 — proposed clang-tidy check, unmerged as of 2026-06-30.
  11. y2k38-checker — cysec-lab, GitHub — Clang Static Analyzer plugin and its four checks.
  12. CID … High impact quality (Y2K38_SAFETY) — ISC BIND 9 issue #3497 — Coverity Y2K38_SAFETY findings in real code.
  13. insane.bbclass (check_32bit_symbols) — OpenEmbedded-Core, GitHub — Yocto 32bit-time QA check and symbol list.
  14. The DATE, DATETIME, and TIMESTAMP Types — MySQL 8.0 Reference Manual — TIMESTAMP range ends 2038-01-19 03:14:07 UTC.
  15. Avoiding the year 2038 problem — GNU Gnulib manual — year2038 / year2038-recommended modules.
  16. Epochalypse Now — or how to deal with Y2K38 — Kaspersky blog, 2026-01-19 — safe testing practice for organisations.
  17. MySQL 8.0.28 Release Notes — Oracle — 64-bit FROM_UNIXTIME()/UNIX_TIMESTAMP() on 64-bit platforms (as cited by Wikipedia).
  18. MariaDB 11.5.1 Release Notes — MariaDB KB — TIMESTAMP extended to 2106 (as cited by Wikipedia).
  19. gmtime, _gmtime32, _gmtime64 — Microsoft Learn — _gmtime32 valid only through 2038-01-18 23:59:59 UTC.