~ / topics / Languages
$man lang-go-rust-swift

Go, Rust and Swift

In brief: Go, Rust and Swift all use 64-bit time in their standard libraries, so normal code is safe well past 2038. Go and Rust programs built for 32-bit Linux are the exception: their low-level system interfaces (Go's syscall package and Rust's libc crate) still use 32-bit time on some targets. Apple's Swift and Objective-C store dates as a floating-point count of seconds from 2001, which has no 2038 limit.

Risk level Low on 64-bit. Medium for Go/Rust on 32-bit Linux (arm, 386, mips)
Status Go: syscall types still 32-bit on linux/386 and arm (issue #60248 open). Rust: libc 64-bit time_t on 32-bit targets is opt-in/unstable as of 0.2.18x. Swift: not affected
Last reviewed 2026-10-21

2038 status at a glance

  • At risk ConfirmedGo syscall / x/sys/unix on 32-bit LinuxTimespec and Timeval are 32-bit on linux/386, arm and mips. The fix (Go issueDo: Avoid raw syscalls on 32-bit targets, and use the time package.
  • DependsRust on 32-bit targets via the libc cratelibc 0.2.178 and earlier use a 32-bit time_t on 32-bit glibc and musl. 0.2.179+ has an unstable 64-bit opt-in.Do: Track the libc crate's time64 support, and test on target.
Not affected or different deadline (2), and why
  • Not affectedGo time packageTime.Unix() returns int64, valid for billions of years. UnixNano() alone is limited to 1678–2262.
  • Not affectedSwift / Objective-C DateA Double count of seconds since 2001.Do: Watch integer conversions such as Int32(timeIntervalSince1970).

Go

time.Time is safe

time.Time.Unix() returns int64. The docs say this is "valid for billions of years into the past or future". The zero Time is January 1, year 1 [1]. time.Duration is an int64 of nanoseconds, so its limit is about 290 years. That matters only for very long durations, not for absolute dates.

Time.UnixNano() is the exception. It returns nanoseconds since 1970 as an int64, and the documentation says "the result is undefined if the Unix time in nanoseconds cannot be represented by an int64 (a date before the year 1678 or after 2262)". That includes the zero Time [1]. This limit belongs to the conversion, not to time.Time. It matters when code stores UnixNano() values as keys, database columns or wire fields, or uses far-future sentinel dates. Use Unix()/UnixMilli(), or keep the time.Time, when a value may be outside 1678–2262.

// BUG: int32 truncation in application code
var expires int32 = int32(time.Now().Unix()) + ttl   // wraps 2038-01-19

// FIX
expires := time.Now().Add(time.Duration(ttl) * time.Second)  // keep time.Time
secs := expires.Unix()                                        // int64 if needed

Go doesn't panic on int32(x) conversions. It truncates silently, like C.

syscall and x/sys/unix on 32-bit platforms

The frozen syscall package mirrors the kernel's legacy 32-bit ABI on 32-bit targets. On linux/386:

type Timespec struct {
    Sec  int32
    Nsec int32
}

Timeval and the Atim/Mtim/Ctim fields of Stat_t are built on the same 32-bit types [2]. Go issue #60248, "syscall: updates types related to 'time_t' to 64-bit on linux/arm" (opened 2023-05-17), notes that ARM, 386 and MIPS all use 32-bit time_t in Go. Ian Lance Taylor outlined the fix: make Time_t 64-bit and switch about 14 functions (ClockGettime, Gettimeofday, Utimes, Ppoll…) to the *_time64 system calls, with fallbacks for old kernels. As of 2026-10-21 the issue is open, labelled help wanted, with no assignee [3]. x/sys/unix tests also treat time.Unix(0x80000000, 0) as unrepresentable in a 32-bit Timespec (seen in an older v0.3.0 tag; current code may differ).

The Go runtime is moving to 64-bit time syscalls on 32-bit Linux. Issue #75133 ("runtime: crash if missing 32-bit time compat on Arm 32-bit Linux") reported that Go crashed on kernels built without CONFIG_COMPAT_32BIT_TIME, because the runtime used the 32-bit futex call. The fix prefers futex_time64 and timer_settime64 and falls back to the 32-bit calls when they're missing. It was closed on 2025-10-21 under the Go 1.26 milestone. A follow-up reported that futex_time64 is blocked by seccomp on Android 10 and earlier (issue #77621) [4].

Practical effect: pure-Go code that uses time and os is fine. Code that calls syscall.Utimes, unix.ClockGettime, unix.Stat and similar on GOARCH=arm/386/mips gets 32-bit seconds. Go also links no libc by default, so Debian's t64 transition does not fix Go binaries.

Rust

std::time::SystemTime

SystemTime is a platform-specific wrapper. "The size of a SystemTime struct may vary depending on the target operating system", and arithmetic "may panic if the underlying structure cannot represent the new point in time". Use checked_add, which returns None when the result can't be represented [5].

use std::time::{Duration, SystemTime, UNIX_EPOCH};

// BUG: as-casts truncate silently
let secs = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i32;

// FIX
let secs: u64 = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
let i32_secs = i32::try_from(secs)?;            // explicit, fallible narrowing
let later = SystemTime::now().checked_add(Duration::from_secs(30 * 365 * 86_400))
    .ok_or("time overflow on this platform")?;

On 32-bit Linux targets, std calls the C library through the libc crate, so SystemTime's range is bounded by the time_t that libc declares.

The libc crate and 32-bit targets

The libc crate can hold one definition of time_t per target, and that definition must match the C library it links against. Its 1.0 tracking issue says: "we can only have one definition of off_t and time_t" [6]. The history:

Date Event
2020-02 musl 1.2.0 makes time_t 64-bit on all archs, ABI-compatible via symbol redirects [7]
2023-07-13 (Rust 1.71) *-linux-musl targets move to musl 1.2.3. Rust says "the libc crate will continue to function correctly without modification", but the crate still declares 32-bit time_t on 32-bit musl [8]
June 2023 musl 1.2.4 removed legacy LFS64 symbols the crate used. Fixed in libc 0.2.146 [9]
2025-12-17 libc PR #4463 "musl: 64-bit time support" merged for x86, arm, mips, powerpc [10]
Early Jan 2026 (0.2.179) Unstable 64-bit time_t on 32-bit musl and glibc, enabled with RUST_LIBC_UNSTABLE_MUSL_V1_2_3=1 / RUST_LIBC_UNSTABLE_GNU_TIME_BITS=64. The changelog heading reads "2025-01-03", which appears to be a typo given the surrounding entries [11]
0.2.187 Environment variables replaced by cfgs: RUSTFLAGS='--cfg=libc_unstable_gnu_time_bits="64"' and --cfg=libc_unstable_musl_v1_2_3 [12]
libc 1.0 (planned) Maintainers intend to default to 64-bit time_t on all platforms [10]

Turning it on for a 32-bit glibc target:

RUSTFLAGS='--cfg=libc_unstable_gnu_time_bits="64"' \
  cargo build --target armv7-unknown-linux-gnueabihf

The release notes give caveats: enabling 64-bit time also enables large-file support, and structs that gained padding can no longer be built with struct-literal syntax [12]. ABI mismatch risk: every crate that binds a C library exposing time_t (for example via bindgen or *-sys crates) must use the same setting as the C library. Otherwise you get the C ABI mismatch described in Lang-C-and-CPP [13].

Separately, Rust 1.93 moves the musl targets to musl 1.2.5. A crater run found about 1.5% of projects affected by the LFS64 symbol removal, most fixable with cargo update [9].

Swift and Objective-C (Apple platforms)

Foundation's Date/NSDate stores a TimeInterval, which is a Double of seconds relative to the reference date 00:00:00 UTC on 1 January 2001 [14]. Apple documents that a TimeInterval "yields sub-millisecond precision over a range of 10,000 years" [15]. So there is no 2038 boundary in Date itself. The constant timeIntervalBetween1970AndReferenceDate converts between the 2001 and 1970 epochs [14].

The risk is in conversions:

let d = Date(timeIntervalSince1970: 2_147_483_648)   // 2038-01-19T03:14:08Z — fine

// BUG: Swift traps (crashes) when a Double doesn't fit the integer type
let secs = Int32(d.timeIntervalSince1970)             // runtime trap

// FIX
let secs64 = Int64(d.timeIntervalSince1970)
guard let secs32 = Int32(exactly: d.timeIntervalSince1970.rounded(.down)) else { /* handle */ }

Swift's default integer-from-float initializer requires that the value be "representable in this type after rounding toward zero". It traps otherwise, while init?(exactly:) returns nil [16]. In Objective-C, (int)[date timeIntervalSince1970] or storing into int32_t is undefined behaviour or silent truncation, as in C.

Other Apple-side spots:

Affected versions / products

Toolchain Safe API 32-bit exposure
Go time Time.Unix() int64 none (but UnixNano() is undefined outside 1678–2262 [1])
Go syscall / x/sys/unix on linux/386, arm, mips — 32-bit Timespec/Timeval (#60248 open)
Go runtime on 32-bit Linux — 64-bit futex/timer syscalls with fallback, Go 1.26 milestone (#75133)
Rust std::time checked_add, u64 secs Bounded by libc time_t on 32-bit
Rust libc ≤ 0.2.178 — 32-bit time_t on 32-bit glibc/musl
Rust libc 0.2.179+ opt-in cfg Unstable 64-bit time_t
Swift / ObjC Date Double since 2001 none (watch integer conversions)

Risk & compliance notes

Detection & mitigation

  1. Go: grep -RnE 'syscall\.(Utimes|Gettimeofday|Timespec|Timeval)|unix\.(Timespec|Timeval|ClockGettime)'. Run tests under GOARCH=arm in QEMU with a clock set past 2038.
  2. Rust: run cargo tree -i libc to see which crates depend on it. Grep for as i32, as u32 and libc::time_t. Enable clippy::cast_possible_truncation.
  3. Swift: search for Int32( and UInt32( around timeIntervalSince*. Prefer Int64 or Date.
  4. See Testing-and-Detection and Remediation-Guide.

See also

References

  1. Package time — Go standard library, pkg.go.dev — Unix() int64 "valid for billions of years"; zero Time is year 1; Duration ~290 years; UnixNano undefined before 1678 or after 2262.
  2. src/syscall/ztypes_linux_386.go — Go source (go.googlesource.com) — Timespec{Sec int32; Nsec int32}, Timeval.
  3. syscall: updates types related to 'time_t' to 64-bit on linux/arm — golang/go issue #60248, 2023-05-17 — open; maintainer comments on 386/arm/mips and the needed syscalls.
  4. runtime: crash if missing 32-bit time compat on Arm 32-bit Linux — golang/go issue #75133 — futex_time64/timer_settime64 fallback; closed 2025-10-21, Go1.26 milestone.
  5. std::time::SystemTime — Rust standard library docs — platform-dependent size and range; checked_add returns None; arithmetic may panic.
  6. Tracking issue for libc 1.0 — rust-lang/libc issue #3248 — single time_t/off_t definition problem; 64-bit time plans.
  7. time64 — musl libc — musl 1.2.0 64-bit time_t on all archs.
  8. Updating musl targets to 1.2.3 — Rust Blog, 2023-05-09 — Rust 1.71 (2023-07-13); libc crate unchanged; 64-bit time definitions deferred.
  9. Updating Rust's Linux musl targets to 1.2.5 — Rust Blog, 2025-12-05 — Rust 1.93; musl 1.2.4 symbol removal fixed in libc 0.2.146; crater ~1.5%.
  10. musl: 64-bit time support — rust-lang/libc PR #4463, merged 2025-12-17 — time64 on x86/arm/mips/powerpc; 1.0 to default to 64-bit time_t.
  11. libc CHANGELOG.md (mirror in Firefox source tree) — searchfox — 0.2.179 unstable 64-bit time_t env vars; dates of surrounding releases.
  12. libc 0.2.187 release notes — NewReleases.io — libc_unstable_gnu_time_bits / libc_unstable_musl_v1_2_3 cfgs; caveats.
  13. gtk-rs/gir issue #1559 — GitHub — Rust bindings must match C library time_t width (seen in search results).
  14. Date — Apple Developer Documentation (Foundation) — reference date 00:00:00 UTC 1 January 2001; timeIntervalBetween1970AndReferenceDate.
  15. TimeInterval — Apple Developer Documentation (Foundation) — typealias TimeInterval = Double; sub-millisecond precision over 10,000 years.
  16. BinaryInteger — Apple Developer Documentation (Swift) — float-to-integer conversion traps if out of range; init?(exactly:) returns nil.