golang/go

os: os.Chtimes is corrupting very old timestamps

Open

#75542 opened on Sep 19, 2025

View on GitHub
 (6 comments) (2 reactions) (0 assignees)Go (133,883 stars) (19,008 forks)batch import
BugReportFixPendingNeedsFixhelp wanted

Description

Go version

go version go1.25.0 linux/amd64

Output of go env in your module/workspace:

AR='ar'
CC='gcc'
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_ENABLED='1'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
CXX='g++'
GCCGO='gccgo'
GO111MODULE=''
GOAMD64='v1'
GOARCH='amd64'
GOAUTH='netrc'
GOBIN=''
GOCACHE='/home/ncw/.cache/go-build'
GOCACHEPROG=''
GODEBUG=''
GOENV='/home/ncw/.config/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFIPS140='off'
GOFLAGS=''
GOGCCFLAGS='-fPIC -m64 -pthread -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=/tmp/go-build512443455=/tmp/go-build -gno-record-gcc-switches'
GOHOSTARCH='amd64'
GOHOSTOS='linux'
GOINSECURE=''
GOMOD='/dev/null'
GOMODCACHE='/home/ncw/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='linux'
GOPATH='/home/ncw/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/opt/go/go1.25'
GOSUMDB='sum.golang.org'
GOTELEMETRY='local'
GOTELEMETRYDIR='/home/ncw/.config/go/telemetry'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/opt/go/go1.25/pkg/tool/linux_amd64'
GOVCS=''
GOVERSION='go1.25.0'
GOWORK=''
PKG_CONFIG='pkg-config'

What did you do?

Running this program demonstrates the problem. It needs to be run on a file system that supports a wide timerange. ext4 doesn't but ntfs-3g does.

package main

import (
	"fmt"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"time"
)

func statAndPrint(label, path string, expected time.Time) {
	fi, err := os.Stat(path)
	if err != nil {
		log.Fatalf("stat (%s): %v", label, err)
	}
	actual := fi.ModTime().UTC()
	fmt.Printf("%s\n", label)
	fmt.Printf("Expected mtime: %sZ (unix nano=%d)\n", expected.Format("2006-01-02T15:04:05"), expected.UnixNano())
	fmt.Printf("Actual   mtime: %s (unix nano=%d)\n", actual.Format(time.RFC3339), actual.UnixNano())
	if expected.Equal(actual) {
		fmt.Printf("ALL OK - times equal\n")
	} else {
		fmt.Printf("FAIL - times differ by %v\n", actual.Sub(expected))
	}
}

func main() {
	if len(os.Args) < 2 {
		log.Fatalf("usage: %s <directory>", os.Args[0])
	}
	dir := os.Args[1]
	fname := filepath.Join(dir, "chtimes_test.txt")

	const expectedStr = "1665-12-01T13:57:13" // interpreted as UTC

	// Create (or truncate) the file
	if err := os.WriteFile(fname, []byte{}, 0o644); err != nil {
		log.Fatalf("create: %v", err)
	}

	// Parse expected time in UTC and set both atime & mtime via os.Chtimes
	expected, err := time.ParseInLocation("2006-01-02T15:04:05", expectedStr, time.UTC)
	if err != nil {
		log.Fatalf("parse: %v", err)
	}
	if err := os.Chtimes(fname, expected, expected); err != nil {
		log.Fatalf("chtimes: %v", err)
	}
	statAndPrint("Set timestamp with os.Chtimes", fname, expected)

	fmt.Printf("------------------------------------------------------------\n")

	// Now set the timestamp via the system `touch` command.
	// Use TZ=UTC so the -t timestamp is not affected by local timezone.
	touchArg := expected.Format("200601021504.05") // CCYYMMDDhhmm.SS
	cmd := exec.Command("touch", "-t", touchArg, fname)
	cmd.Env = append(os.Environ(), "TZ=UTC")
	if out, err := cmd.CombinedOutput(); err != nil {
		log.Fatalf("touch: %v; output: %s", err, string(out))
	}
	statAndPrint("Set timestamp with touch (TZ=UTC)", fname, expected)
}

You can create a suitable file system to check this on with an NTFS-3g loopback on Linux like this

truncate -s 10M ntfs.img
sudo mkfs.ntfs -F -L NTFS_VOL ntfs.img
sudo mkdir -p /mnt/ntfsimg
sudo mount -t ntfs-3g -o loop,uid=$UID,gid=$(id -g) ntfs.img /mnt/ntfsimg

What did you see happen?

It attempts to set a 1665 date as a timestamp on a file. This is read back as a 2250 timestamp.

Set timestamp with os.Chtimes
Expected mtime: 1665-12-01T13:57:13Z (unix nano=8850864706709551616)
Actual   mtime: 2250-06-22T13:31:46Z (unix nano=8850864706709551600)
FAIL - times differ by 2562047h47m16.854775807s

It then sets the timestamp with touch which does work as expected (showing that this date is representable by the file system and by Go time.Time).

Set timestamp with touch (TZ=UTC)
Expected mtime: 1665-12-01T13:57:13Z (unix nano=8850864706709551616)
Actual   mtime: 1665-12-01T13:57:13Z (unix nano=8850864706709551616)
ALL OK - times equal

Note how all the unix nano values are the same (+/- 16nS)

What did you expect to see?

I expected the go standard library not to lose the extra precision in the Go timestamp when applying it to the file.

The problem appears to be here

https://github.com/golang/go/blob/3cf1aaf8b9c846c44ec8db679495dd5816d1ec30/src/os/file_posix.go#L179-L199

In particular the use of UnixNano() to convert the time as the 1665 date is not representable as an int64 nS since the epoch but it is representable as a syscall.Timespec

type Timespec struct {
	Sec  int64
	Nsec int64
}

This could probably fixed by constructing the Timespec something like this instead of utimes[i] = syscall.NsecToTimespec(t.UnixNano())

utimes[i] = syscall.Timespec{
    Sec: t.Unix(),
    Nsec: int64(t.Nanosecond()),
}

Though I am uncertain as to whether there is any rounding in t.Unix() which might affect things.

Note that this likely affects Windows too as the time is roundtripped through an int64 there too

https://github.com/golang/go/blob/3cf1aaf8b9c846c44ec8db679495dd5816d1ec30/src/os/root_windows.go#L367-L372

This problem was discovered by an rclone user in https://github.com/rclone/rclone/issues/8834

Contributor guide