We Could Get Stuck Here: A Hands-On Field Manual for Y2038

The Unix epoch overflows at 03:14:07 UTC on January 19, 2038. The disaster is not that systems break in 2038 — it is that data being written into broken schemas today will not surface as wrong until the wraparound. Here is exactly what to audit, with code, on the box.

We Could Get Stuck Here: A Hands-On Field Manual for Y2038

The Unix epoch overflows at 03:14:07 UTC on January 19, 2038. That is real. The fix is straightforward. The disaster comes from not auditing now — because the systems most likely to break in 2038 are the ones nobody is touching today. Here is exactly what to do, with code, on the box.


What actually breaks

Original Unix stored time as a signed 32-bit integer counting seconds since 1970-01-01 00:00:00 UTC. This type is called time_t. The maximum value is:

2^31 − 1 = 2,147,483,647

That many seconds after the epoch lands at 03:14:07 UTC on Tuesday, January 19, 2038. One second later the sign bit flips and the value becomes −2,147,483,648, which the same code reads as 20:45:52 UTC on Friday, December 13, 1901.

Sort orders invert. Comparisons reverse. Cron jobs scheduled “in the future” become “in the past.” Session expiries flip from not yet expired to expired by 137 years. Certificates compute negative remaining lifetime. Filesystems write timestamps that read as 1901. Backup tools see “new” files as older than “old” files and overwrite the wrong ones. Database queries that filter by created_at > NOW() - 30 days silently include or exclude every record depending on which side of the wraparound you sit on.

The bug is not that “computers will stop working in 2038.” The bug is that everything that depends on the relative ordering of timestamps starts lying, and a great deal of it keeps running. That is worse.

Why this is already a disaster, not a future one

You start writing dates into your storage that will overflow long before 2038 actually arrives. Some examples already in production today:

  • A session-token table that stores expiry as INT (32-bit signed) in MySQL, with sessions issued for “10 years from now.” On January 19, 2028, every new session you write already has an expiry past the wraparound.
  • A TLS certificate authority issuing 13-year certificates today. The cert is valid until 2039 — but if anything in your stack reads notAfter through a 32-bit time_t path, the cert reads as already-expired the moment it’s parsed.
  • An industrial PLC firmware flashed in 2018 with a 15-year support window. The vendor is still in business in 2030 when the bug is finally identified, but the device has been deployed to 40,000 sites and nobody is going to replace it. There is no over-the-air update path because that capability was a 2024 product line.
  • A cron job that schedules “next run = now + 6 months.” Run after July 2037, the next-run timestamp it writes is past the wraparound.

The 2038 boundary is the moment everything visibly snaps. The data that was already written wrong has been quietly accumulating for years.

The six surfaces where you’re exposed

Audit your stack against each of these. In rough order of how-likely-it-is-already-broken-in-your-environment:

  1. Application code — anywhere you cast time(NULL) to int, store epoch seconds in an int column, marshal time as 32-bit in a binary protocol, or assume sizeof(time_t) == 4.
  2. DatabasesINT or INT(11) columns holding epoch seconds. MySQL TIMESTAMP type (storage is 32-bit signed, max value 2038-01-19 03:14:07). Older ETL pipelines that round-trip times through 32-bit integers.
  3. Filesystemsext2, ext3, and ext4 with 128-byte inodes. FAT32 (different bug, max year 2107, but loses second-precision). Old tar archive formats (ustar headers cap at 2242 if the implementation is correct, earlier if not).
  4. libc and the kernel — 32-bit systems with old glibc store time_t as 32-bit. The _TIME_BITS=64 mechanism exists since glibc 2.32 (August 2020) but you have to opt in at compile time on 32-bit platforms. musl libc moved all platforms to 64-bit time_t in 1.2.0 (February 2020). Linux kernel “Y2038-clean” since 5.6 (March 2020).
  5. Embedded firmware — anything with a flashed binary, no update path, and a 32-bit time field in its log/event/sensor data structures. Industrial control, medical, automotive ECUs, satellites, building automation. This is the slowest surface to remediate and the one most likely to cause real harm.
  6. Network protocols and on-wire formats — NTPv4 has its own 32-bit-seconds rollover in 2036 (separate from Y2038, two years earlier), handled by era counting in correctly-updated daemons. SNMPv2 and older protocols carry 32-bit time fields. NFSv3 file timestamps are 32-bit signed.

PostgreSQL is fine: its TIMESTAMP type is 64-bit microseconds since 2000-01-01, valid to roughly the year 294,277. NTFS is fine: 64-bit 100-nanosecond ticks since 1601, valid to 30828. APFS is fine: 64-bit nanoseconds since 1970. ZFS is fine. Btrfs is fine. XFS v5 (default since 2014, requires CRC) is fine.

The question is not “is my OS Y2038-clean.” The question is “is every layer of my stack Y2038-clean, including the ones I inherited.”

Detection: code you can run on the box right now

1. What size is time_t on this machine?

Save as time_size.c:

#include <stdio.h>
#include <time.h>
#include <stdint.h>
#include <limits.h>

int main(void) {
    printf("sizeof(time_t)        = %zu bytes (%zu bits)\n",
           sizeof(time_t), sizeof(time_t) * CHAR_BIT);
    printf("sizeof(struct timespec) = %zu bytes\n",
           sizeof(struct timespec));

    time_t now = time(NULL);
    printf("time(NULL)            = %lld\n", (long long)now);
    printf("ctime(now)            = %s", ctime(&now));

    /* Demonstrate the boundary */
    time_t boundary = (time_t)2147483647;
    printf("Y2038 boundary        = %s", ctime(&boundary));

    /* If sizeof(time_t) == 4, this overflows */
    time_t after = boundary + 1;
    printf("Boundary + 1 second   = %s", ctime(&after));

    return 0;
}

Build and run:

gcc -Wall -O2 time_size.c -o time_size
./time_size

On any 64-bit Linux, macOS, or modern BSD you should see sizeof(time_t) = 8 bytes (64 bits) and the “Boundary + 1 second” line reading Tue Jan 19 03:14:08 2038. If you see 4 bytes (32 bits) and the line reads Fri Dec 13 20:45:52 1901, you are on a 32-bit system with 32-bit time_t and everything compiled against that libc on this box is exposed.*

To rebuild a 32-bit binary with 64-bit time_t on a sufficiently new glibc (2.32+):

gcc -D_TIME_BITS=64 -D_FILE_OFFSET_BITS=64 -Wall -O2 time_size.c -o time_size64
./time_size64

_FILE_OFFSET_BITS=64 has to ride along because the kernel ABI for several syscalls couples them.

2. What does the libc and kernel report?

ldd --version | head -1
uname -srm
getconf LONG_BIT
getconf -a | grep -i time

If LONG_BIT is 64 you are almost certainly fine at the libc level. If it is 32, check the glibc version (>= 2.32 lets you compile with _TIME_BITS=64).

3. What about Python?

Python’s time.time() returns a float, which has enough precision to represent seconds past 2038 fine. The problem is anywhere you cast to int and then store in a 32-bit field:

import struct, time

# This is the smoking gun for Y2038 in Python code:
ts = int(time.time())
packed = struct.pack('!i', ts)   # signed 32-bit big-endian
# After 2038-01-19 03:14:07, the line above raises:
#   struct.error: argument out of range

Search your codebase for struct.pack and struct.unpack format characters i (signed 32-bit) and I (unsigned 32-bit) anywhere near a timestamp. Switch to q/Q (signed/unsigned 64-bit):

packed = struct.pack('!q', ts)   # signed 64-bit, good to year 292,277,026,596

4. What about my databases?

MySQL / MariaDB:

-- Find every TIMESTAMP column in every user database. These are 32-bit.
SELECT table_schema, table_name, column_name, column_type
FROM information_schema.columns
WHERE data_type = 'timestamp'
  AND table_schema NOT IN ('mysql','sys','performance_schema','information_schema');

-- Find INT-typed columns that look like they hold epoch seconds.
SELECT table_schema, table_name, column_name, column_type
FROM information_schema.columns
WHERE data_type IN ('int','mediumint','smallint')
  AND (column_name LIKE '%_at'
       OR column_name LIKE '%time%'
       OR column_name LIKE '%expir%'
       OR column_name LIKE 'ts'
       OR column_name LIKE 'epoch%')
ORDER BY table_schema, table_name;

PostgreSQL:

SELECT table_schema, table_name, column_name, data_type
FROM information_schema.columns
WHERE table_schema NOT IN ('pg_catalog','information_schema')
  AND (data_type IN ('integer','int','smallint'))
  AND (column_name ~ '_at$' OR column_name ~ 'time' OR column_name ~ 'expir' OR column_name ~ 'epoch');

PostgreSQL’s native timestamp types are not the issue. The issue is your own integer columns.

5. What about my filesystems?

For ext-family filesystems:

sudo dumpe2fs -h /dev/sdX1 | grep -i 'inode size\|features'

Look for Inode size: 256 (or larger). 128-byte inodes are the old layout and will not survive 2038 — they lack the extra bits used to extend the timestamp range.

For XFS:

sudo xfs_info /mountpoint | grep -i 'crc\|bigtime'

bigtime=1 means 64-bit timestamps, valid to 2486. The crc=1 (v5) format added the capability; xfs_admin -O bigtime /dev/sdX enables it on existing v5 filesystems.

For ZFS, Btrfs, APFS, NTFS: native 64-bit timestamps from creation. Verify but you are almost certainly fine.

Fixes by surface

Application code

The general pattern: stop coercing time to 32-bit anywhere. Cast through int64_t / long long, store in BIGINT, marshal as 8-byte fields, pack with format chars that span 64 bits.

Before:

struct event_record {
    int32_t timestamp;   /* seconds since epoch */
    int32_t event_code;
    char payload[120];
};

void log_event(int code, const char *payload) {
    struct event_record r;
    r.timestamp = (int32_t)time(NULL);  /* DEAD IN 2038 */
    r.event_code = code;
    strncpy(r.payload, payload, sizeof(r.payload) - 1);
    fwrite(&r, sizeof r, 1, fp);
}

After:

struct event_record {
    int64_t timestamp;   /* seconds since epoch */
    int32_t event_code;
    char    payload[120];
};

void log_event(int code, const char *payload) {
    struct event_record r;
    r.timestamp = (int64_t)time(NULL);  /* clean to year 292B */
    r.event_code = code;
    strncpy(r.payload, payload, sizeof(r.payload) - 1);
    fwrite(&r, sizeof r, 1, fp);
}

If you have a binary log file already in the 32-bit format and you want to keep it readable, write a migration tool that walks the old file, sign-extends the 32-bit value to 64-bit, and writes a new file with the new layout. A version byte at the head of each record is cheap insurance.

Python pattern audit (run from repo root):

# Find suspicious int casts of time
grep -rn 'int(time\.' --include='*.py' .

# Find 32-bit struct format chars near timestamp variables
grep -rn "struct\.\(pack\|unpack\).*['\"][^'\"]*[iI]" --include='*.py' .

# Find Pydantic / ORM columns typed Integer-not-BigInteger near timestamps
grep -rnE "Column\(Integer.*(time|_at|expir|epoch)" --include='*.py' .

Databases

MySQL — convert TIMESTAMP to DATETIME (or BIGINT epoch ms):

-- DATETIME stores Y-M-D h:m:s packed, valid 1000 to 9999. No Y2038 bug.
ALTER TABLE sessions
  MODIFY COLUMN created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  MODIFY COLUMN expires_at DATETIME NOT NULL;

-- Or, if you want to keep epoch semantics, go to BIGINT.
ALTER TABLE events
  MODIFY COLUMN ts_epoch BIGINT NOT NULL;

Caveats when migrating MySQL TIMESTAMPDATETIME: TIMESTAMP is stored in UTC and converted to/from the session time zone on read/write. DATETIME is stored as written, no timezone awareness. If your application depended on the implicit UTC conversion, you need to standardize all writes to UTC explicitly. A common, safer migration is TIMESTAMP → BIGINT (epoch milliseconds), which preserves UTC-on-disk semantics and gives you sub-second precision.

On-the-fly check before migrating: verify no existing data already overflows the source type’s domain:

SELECT COUNT(*) FROM events WHERE ts_epoch < 0 OR ts_epoch > 4294967295;

PostgreSQL — your timestamp types are already 64-bit. Just hunt down any integer columns you typed manually:

ALTER TABLE events ALTER COLUMN ts_epoch TYPE BIGINT;

Filesystems

If you have any ext4 partitions with 128-byte inodes, the fix is a backup-and-recreate cycle: mkfs.ext4 -I 256 and restore. There is no in-place inode-size upgrade tool that is safe to run on a live filesystem. Plan the outage now while you can choose the date.

For XFS v5 without bigtime:

# Unmount first.
sudo umount /mnt/data
sudo xfs_admin -O bigtime /dev/sdX1
sudo mount /mnt/data

For FAT32 / exFAT — these have their own timestamp ranges (FAT32 caps at 2107) and you are likely using them on removable media where the easy answer is “don’t store data that matters here long-term.”

Embedded / IoT firmware

This is the surface where the hands-on field manual reaches its limit. If you control the firmware build:

/* Make sure your build configures 64-bit time_t */
#if !defined(_TIME_BITS) || _TIME_BITS < 64
#  error "Build must define _TIME_BITS=64 for Y2038-clean operation"
#endif

If you do not control the firmware build, document the device, mark its end-of-life as 2038-01-19, and plan for replacement. Some devices can be partially mitigated by setting their clock perpetually behind real time (a “time-warp” workaround), but this corrupts forensic timelines and breaks anything that depends on real wall-clock semantics.

NTP

Run a recent chrony (4.0+) or ntpd (4.2.8p15+) — both handle the 2036 NTP-era rollover and the 2038 system-time rollover. If you are running an OpenWrt router with a 2016-era ntpd baked into its firmware, see “embedded firmware” above.

# Check on a Linux box
chronyd -v   # or: ntpd --version

The Hercules angle: debugging legacy stacks before they bite

A great deal of critical infrastructure has IBM System/360, System/370, and z/Architecture mainframe code running underneath modern wrappers. The original code is often in COBOL, PL/I, Assembler, or APL, written between 1965 and 1985, with the original developers retired or dead. When the Y2038 boundary hits a Unix host layer that this legacy code talks to through a foreign-function interface, the fix has to touch the legacy code. You cannot do that without a working development environment for it.

Hercules is a free, open-source emulator for the entire S/360–S/370–ESA/390–z/Architecture family. It runs on any modern Linux/macOS/Windows host, and combined with a public-domain operating system distribution like MVS 3.8j (the last freely-redistributable IBM mainframe OS, c. 1981) or Turnkey MVS (TK4-), you have a complete environment for reading, instrumenting, and patching legacy mainframe code.

This is also the technical core of the John Titor narrative: the claim was that future engineers needed the IBM 5100 specifically because by 2036 nobody could debug the layered APL/COBOL stack anymore. The framing was time-travel fiction. The underlying technical problem — that legacy mainframe code is real, critical, and increasingly opaque — is not.

Setting up Hercules

On Debian/Ubuntu:

sudo apt update
sudo apt install hercules
hercules --version

Or build from source (more current features):

git clone https://github.com/SDL-Hercules-390/hyperion.git
cd hyperion
./util/bldlvlck
./configure --enable-optimization=yes
make -j$(nproc)
sudo make install

Booting MVS 3.8j on Hercules

Pull a Turnkey distribution. The classic is TK4-, which bundles MVS 3.8j with a curated set of compilers and utilities:

mkdir -p ~/mainframe && cd ~/mainframe
wget http://wotho.ethz.ch/tk4-/tk4-_v1.00_current.zip
unzip tk4-_v1.00_current.zip -d tk4
cd tk4
./mvs            # starts Hercules and IPLs MVS 3.8j

In another terminal, attach a 3270 terminal emulator (x3270 or c3270):

sudo apt install x3270
x3270 localhost:3270 &

Log on as HERC01 / CUL8TR (TK4-’s default user). You now have a working MVS 3.8j environment with COBOL, PL/I, Assembler, and an APL interpreter.

Hercules console: the actual debugging surface

The Hercules console understands a set of commands for inspection and tracing the running mainframe. Useful subset for Y2038-style audits:

hercules> stop                  # halt the CPU
hercules> r 100.40              # display 64 bytes at address 0x100
hercules> r0                    # display general-purpose register 0
hercules> psw                   # display program status word
hercules> v r 100               # virtual-to-real address resolution
hercules> tr +I 1A              # turn on instruction trace for opcode 0x1A
hercules> tr +s 200             # break on storage access at 0x200
hercules> start                 # resume execution
hercules> g                     # go (alias)

The pattern for a Y2038 audit on legacy mainframe code:

  1. Identify the time-handling routine. In MVS, the canonical “current time” service is the TIME macro, which under the covers invokes the STCK (Store Clock) or STCKE (Store Clock Extended) instruction. STCK returns a 64-bit value. STCKE returns 128 bits. Both are immune to the Unix Y2038 bug because mainframe time is rooted at 1900-01-01 00:00:00 UTC and measured in 2⁻¹² microseconds, not Unix seconds.
  2. Find the interop layer. The bug enters a mainframe stack when it talks to a non-mainframe peer — a Unix host doing data exchange, a TCP/IP stack carrying timestamps, a downstream Java/C process that consumes mainframe output. Audit any code path that converts mainframe TOD (Time-Of-Day clock) to Unix epoch.
  3. Watch the conversion. A common conversion routine subtracts the mainframe-vs-Unix epoch offset (70 years, 17 leap days, expressed as a TOD-clock constant) and divides by 4,096,000 to get Unix seconds, then stores the result. If that store is to a 32-bit field — that is your Y2038 site.

Hercules instruction trace makes this catchable. Set a trace on storage writes to the conversion target buffer, watch the value being written, see whether the field is 4 bytes or 8.

APL on a mainframe under Hercules

APL on a System/370 environment runs from datasets that you IPLd as part of the MVS bring-up (TK4- includes APL.SV, a public-domain APL\360 implementation). The relevant native time facility in APL is ⎕TS — system timestamp — which returns a 7-element vector:

      ⎕TS
2026 5 27 11 47 23 412

(year month day hour minute second millisecond). APL’s native timestamp has no Y2038 bug — the year is a full integer, not seconds-since-epoch. The bug enters APL code only when the APL program calls out to an OS time service that returns Unix seconds, or stores its own timestamp as an integer offset from a fixed epoch in a 32-bit-bounded field.

This is, in fact, the cleanest part of the legacy stack from a Y2038 standpoint. The mess is at the boundaries — wherever mainframe TOD or APL ⎕TS gets converted into something a Unix-side consumer expects.

A worked example: instrumenting a COBOL date-conversion routine

Suppose you have COBOL code under MVS that converts the mainframe TOD to a Unix-format timestamp and writes it to a record passed to a Java consumer over MQ:

       IDENTIFICATION DIVISION.
       PROGRAM-ID. UTSCONV.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  TOD-DATA            PIC X(8).
       01  UNIX-SECS           PIC S9(9) COMP.        *> 32-bit signed
       01  TOD-EPOCH-OFFSET    PIC X(8) VALUE
           X'7D91048BCA000000'.                       *> 1970 in TOD units
       PROCEDURE DIVISION.
           CALL 'CEELOCT' USING TOD-DATA.            *> get current TOD
           ...                                       *> subtract offset
           MOVE COMPUTED-VALUE TO UNIX-SECS.
           DISPLAY 'UTS = ' UNIX-SECS.
           GOBACK.

UNIX-SECS declared as PIC S9(9) COMP is a 32-bit signed binary field. That is the Y2038 site. The fix:

       01  UNIX-SECS           PIC S9(18) COMP.       *> 64-bit signed

PIC S9(18) COMP (or COMP-5 for native byte order) is at least 64 bits on every modern z/OS COBOL implementation. Recompile, rebind, redeploy. The Java consumer downstream needs to be reading the field as long not int, but that is its own audit.

Under Hercules you can set a tracepoint on the storage write to verify the new layout writes 8 bytes:

hercules> tr +s UNIX-SECS-ADDR
hercules> g
... breakpoint hit, displays before/after, confirm 8-byte write

If you do not have the COBOL source — and this is common for code that was last touched in 1987 — the workflow becomes: dump the load module, disassemble with a tool like IDADIS or extract from IEBPTPCH, identify the binary store instructions writing to the conversion buffer, patch the load module to use STG (store 64-bit) instead of ST (store 32-bit), test under Hercules, then promote. This is a real job, and it is the kind of job that has to be done before 2038 in a great many institutional stacks.

The audit checklist

Walk this list across each system you own. Schedule it for a quiet Sunday. The hands-on commands are listed under each item.

1. Host OS time width

gcc -x c -o /tmp/timesize - <<'EOF' && /tmp/timesize
#include <stdio.h>
#include <time.h>
int main(){printf("time_t=%zu bytes\n",sizeof(time_t));return 0;}
EOF

2. libc + kernel version

ldd --version | head -1
uname -srm

3. NTP daemon

chronyd -v 2>/dev/null || ntpd --version 2>/dev/null

4. Filesystems

# For each mounted FS
for fs in $(awk '$3 ~ /^ext|^xfs/ {print $1}' /proc/mounts); do
  echo "=== $fs ==="
  if [ "${fs#/dev}" != "$fs" ]; then
    sudo dumpe2fs -h "$fs" 2>/dev/null | grep -i 'inode size\|features' || \
    sudo xfs_info "$(awk -v dev=$fs '$1==dev{print $2;exit}' /proc/mounts)" | grep -i 'crc\|bigtime'
  fi
done

5. Application code grep

# C/C++
grep -rn 'time_t' --include='*.c' --include='*.h' --include='*.cpp' . | grep -v 'time_t.*64\|int64'
grep -rn '(int)time(' --include='*.c' --include='*.h' --include='*.cpp' .

# Python
grep -rn 'int(time\.' --include='*.py' .
grep -rnE "struct\.(pack|unpack).*['\"][^'\"]*[iI]" --include='*.py' .

# Go (the language has int64 timestamps natively; risk is encoding)
grep -rn 'int32.*[Tt]ime' --include='*.go' .

# Rust (i32 timestamps)
grep -rn 'i32.*time\|time.*i32' --include='*.rs' .

# Java (int millis or int seconds)
grep -rnE '\bint\b.*[Tt]ime(stamp|Secs|Epoch)' --include='*.java' .

6. Database schemas

Use the SQL queries from the Detection section against every database you own. Treat the output as a punch list.

7. Binary protocols and on-wire formats

Audit any Protocol Buffer .proto, Apache Thrift .thrift, Cap’n Proto schema, FlatBuffers schema, or hand-rolled binary record format for int32 / i32 / INT fields near timestamp semantics. Promote to int64 / sfixed64 / BIGINT everywhere.

8. Certificates

# Find certs with notAfter past 2038 already in your store
for cert in /etc/ssl/certs/*.pem; do
  na=$(openssl x509 -in "$cert" -noout -enddate 2>/dev/null | cut -d= -f2)
  if [ -n "$na" ]; then
    epoch=$(date -d "$na" +%s 2>/dev/null)
    if [ "$epoch" -gt 2147483647 ] 2>/dev/null; then
      echo "POST-2038: $cert  notAfter=$na"
    fi
  fi
done

If your TLS terminator is on a 32-bit time_t system, certificates with notAfter > 2038-01-19 03:14:07 UTC will appear already-expired when parsed through any 32-bit code path.

9. Embedded inventory

For each device with flashable firmware in your environment, record: vendor, model, firmware version, time-handling design (where documented), upgrade path. Devices with no upgrade path and 32-bit time fields are scheduled to fail on 2038-01-19 regardless of what you do today. Plan replacement.

10. Run a date-set test in a sandbox

The most direct way to find Y2038 bugs is to set the system clock past the boundary and run your application:

# IN A CONTAINER OR VM, NEVER ON A PRODUCTION HOST
sudo date -s '2038-01-19 03:14:00 UTC'
# wait 10 seconds
sudo date            # should now read past the boundary
# run your test suite

What you are looking for: silent failures, comparison reversals, sort-order inversions, “stale” cache entries that should be fresh, “fresh” cache entries that should be stale. Anything that depends on time ordering and silently produces the wrong answer is your bug.

What you should actually be unaware-of-but-aware-of

  • The 2036 NTP rollover comes first. Two years before Y2038. If your NTP daemon is from before ~2016, it will probably handle it via era counting, but verify. A misbehaving NTP daemon at the 2036 rollover can step the system clock backwards by 136 years on a fleet of machines simultaneously, which is a much louder failure mode than a quiet 2038 overflow.
  • TLS certificate authorities are not issuing certs past 2038 on the public web. They will. The CA/B Forum will adjust validity-period rules. But auditing your own internal CAs for 32-bit notAfter handling is on you.
  • The Linux kernel ABI is Y2038-clean since 5.6. Old userspace on a new kernel can still be exposed. New userspace on an old kernel is exposed at the syscall boundary. Updating to a current LTS kernel and a current libc together is the durable answer for the OS layer.
  • Hercules is freely available and runs everything from S/360 to z/Architecture. TK4- gives you MVS 3.8j with compilers. If you ever inherit a stack with mainframe code underneath and you need to audit it, you do not need to find an actual mainframe.
  • The most accurate technical claim John Titor ever made was about this exact problem. The IBM 5100 had a hidden System/370 emulator. That was confirmed years after his posts by IBM engineers like Bob Dubke. The time-traveler frame was fiction. The underlying technical observation — that legacy mainframe code running under modern wrappers is the hard-to-debug part of the 2038 problem — was correct. The fix is not a time machine. The fix is Hercules, a Sunday afternoon, and the audit checklist above.
  • Run the audit now, not in 2037. The reason 2038 is already a disaster is that data being written into broken schemas today will not surface as broken until the wraparound, and by then the records-of-record have been wrong for a decade. The remediation cost compounds. The remediation cost of fixing it this year is small. The remediation cost of fixing it in 2037 is enormous, and the cost in 2038 — for systems that did not get fixed in time — is paid in correctness, not effort.

The disaster is not “the systems break in 2038.” The disaster is “the systems have been quietly lying since the day we wrote the bad schema.” Audit. Migrate. Recompile. Document the irreducible exposures. Set the alarm.

We do not get stuck here.


Tooling referenced in this post: Hercules emulator (SDL-Hercules-390), Turnkey MVS 3.8j (TK4-), x3270 terminal emulator, glibc _TIME_BITS documentation, Linux kernel Y2038 design, Linus Torvalds’ commit moving time_t to 64-bit on 32-bit platforms. The audit scripts and code samples in this post are released into the public domain — copy, paste, adapt, ship.

subscribe for amazing dishes served hot. no spam, just quick info- appetizers | entrees | desserts | snacks of course! :-)