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
syscallpackage and Rust'slibccrate) 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:
time_t,struct statandgettimeofdayin C or Objective-C code follow the platform ABI. See OS-macOS-and-iOS for which Apple ABIs had 32-bittime_t.- Core Data, SQLite and CloudKit schemas with
Integer 32attributes that hold epoch seconds. - Swift on Linux uses swift-corelibs-foundation over glibc, so a 32-bit Linux Swift build inherits glibc's
time_tchoice.
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
- Inventory every Go or Rust binary built for
GOARCH=arm|386|mips*orarmv7-*/i686-*/mips-*Rust targets, especially IoT agents, routers and gateways. - Go and Rust binaries are statically linked, so OS-level fixes don't help them. They must be rebuilt with fixed toolchains or crates.
- Owners: firmware teams, and maintainers of
*-syscrates and cgo bindings.
Detection & mitigation
- Go:
grep -RnE 'syscall\.(Utimes|Gettimeofday|Timespec|Timeval)|unix\.(Timespec|Timeval|ClockGettime)'. Run tests underGOARCH=armin QEMU with a clock set past 2038. - Rust: run
cargo tree -i libcto see which crates depend on it. Grep foras i32,as u32andlibc::time_t. Enableclippy::cast_possible_truncation. - Swift: search for
Int32(andUInt32(aroundtimeIntervalSince*. PreferInt64orDate. - See Testing-and-Detection and Remediation-Guide.
See also
References
- Package time — Go standard library, pkg.go.dev —
Unix()int64 "valid for billions of years"; zero Time is year 1; Duration ~290 years;UnixNanoundefined before 1678 or after 2262. - src/syscall/ztypes_linux_386.go — Go source (go.googlesource.com) —
Timespec{Sec int32; Nsec int32},Timeval. - 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.
- 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.
- std::time::SystemTime — Rust standard library docs — platform-dependent size and range;
checked_addreturnsNone; arithmetic may panic. - Tracking issue for libc 1.0 — rust-lang/libc issue #3248 — single
time_t/off_tdefinition problem; 64-bit time plans. - time64 — musl libc — musl 1.2.0 64-bit
time_ton all archs. - 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.
- 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%.
- 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. - libc CHANGELOG.md (mirror in Firefox source tree) — searchfox — 0.2.179 unstable 64-bit
time_tenv vars; dates of surrounding releases. - libc 0.2.187 release notes — NewReleases.io —
libc_unstable_gnu_time_bits/libc_unstable_musl_v1_2_3cfgs; caveats. - gtk-rs/gir issue #1559 — GitHub — Rust bindings must match C library
time_twidth (seen in search results). - Date — Apple Developer Documentation (Foundation) — reference date 00:00:00 UTC 1 January 2001;
timeIntervalBetween1970AndReferenceDate. - TimeInterval — Apple Developer Documentation (Foundation) —
typealias TimeInterval = Double; sub-millisecond precision over 10,000 years. - BinaryInteger — Apple Developer Documentation (Swift) — float-to-integer conversion traps if out of range;
init?(exactly:)returns nil.