From eaaaaee1dc1aacededf4a89bc4544558f425d5f1 Mon Sep 17 00:00:00 2001 From: gremat <50012463+gremat@users.noreply.github.com> Date: Mon, 2 Mar 2026 08:57:12 +0000 Subject: [PATCH] windows/registry: correct KeyInfo.ModTime calculation Filetime.Nanoseconds() has a major drawback: the returned int64 is too small to represent Filetime's zero value (January 1, 1601, [1]) in terms of nanoseconds since Epoch (00:00:00 UTC, January 1, 1970); MinInt64 [2] only dates back to year 1677. This has real-life implications, e.g., some Windows sub systems (Perflib, to name one) create registry keys with the last write time property set to zero (see note below). In this case, ModTime() reports an underflow-affected value of 2185-07-22T00:34:33.709551+01:00. This commit drops usage of Nanoseconds() in favor of a conversion that converts first to seconds and nanoseconds before gauging thus is capable to cover the full range of Filetime values. A note on last write time values: `lastWriteTime` is not exposed in the UI (say, `regedit`) or in PowerShell (`Get-ItemProperty`), you need to query `RegQueryInfoKeyA` [3] explicitly in some way [4] or another [5]. The source of the latter is offline by now but can be found elsewhere [6] and provides a quick way to show the value. [1] https://learn.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-filetime [2] https://pkg.go.dev/math#pkg-constants [3] https://learn.microsoft.com/en-us/windows/win32/api/winreg/nf-winreg-regqueryinfokeya?redirectedfrom=MSDN [4] https://learn.microsoft.com/en-us/windows/win32/sysinfo/retrieving-the-last-write-time [5] https://learn-powershell.net/2014/12/18/retrieving-a-registry-key-lastwritetime-using-powershell/ [6] https://github.com/wxrdnx/GetRegistryKeyLastWriteTimeAndClassName Fixes golang/go#74335. Change-Id: I83dc1d6b5e0c581bfb53d58ee08a21ed9c166b0f GitHub-Last-Rev: 0073d1d313b2c986360f3a06ffeceb6b331c0d9a GitHub-Pull-Request: golang/sys#251 Reviewed-on: https://go-review.googlesource.com/c/sys/+/682816 Reviewed-by: Alex Brainman LUCI-TryBot-Result: Go LUCI Reviewed-by: Russ Cox Reviewed-by: David Chase --- windows/registry/export_test.go | 4 ++++ windows/registry/key.go | 15 ++++++++++++++- windows/registry/registry_test.go | 28 ++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/windows/registry/export_test.go b/windows/registry/export_test.go index 7f1ac70e..f3a30e24 100644 --- a/windows/registry/export_test.go +++ b/windows/registry/export_test.go @@ -9,3 +9,7 @@ package registry func (k Key) SetValue(name string, valtype uint32, data []byte) error { return k.setValue(name, valtype, data) } + +func (ki *KeyInfo) ModTimeZero() bool { + return ki.modTimeZero() +} diff --git a/windows/registry/key.go b/windows/registry/key.go index 39aeeb64..7cc6ff3a 100644 --- a/windows/registry/key.go +++ b/windows/registry/key.go @@ -198,7 +198,20 @@ type KeyInfo struct { // ModTime returns the key's last write time. func (ki *KeyInfo) ModTime() time.Time { - return time.Unix(0, ki.lastWriteTime.Nanoseconds()) + lastHigh, lastLow := ki.lastWriteTime.HighDateTime, ki.lastWriteTime.LowDateTime + // 100-nanosecond intervals since January 1, 1601 + hsec := uint64(lastHigh)<<32 + uint64(lastLow) + // Convert _before_ gauging; the nanosecond difference between Epoch (00:00:00 + // UTC, January 1, 1970) and Filetime's zero offset (January 1, 1601) is out + // of bounds for int64: -11644473600*1e7*1e2 < math.MinInt64 + sec := int64(hsec/1e7) - 11644473600 + nsec := int64(hsec%1e7) * 100 + return time.Unix(sec, nsec) +} + +// modTimeZero reports whether the key's last write time is zero. +func (ki *KeyInfo) modTimeZero() bool { + return ki.lastWriteTime.LowDateTime == 0 && ki.lastWriteTime.HighDateTime == 0 } // Stat retrieves information about the open key k. diff --git a/windows/registry/registry_test.go b/windows/registry/registry_test.go index 6e7bec50..995acc96 100644 --- a/windows/registry/registry_test.go +++ b/windows/registry/registry_test.go @@ -9,6 +9,7 @@ package registry_test import ( "bytes" "crypto/rand" + "errors" "os" "syscall" "testing" @@ -674,3 +675,30 @@ func GetDynamicTimeZoneInformation(dtzi *DynamicTimezoneinformation) (rc uint32, } return } + +func TestModTimeZeroValue(t *testing.T) { + k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion\Perflib\009`, registry.READ) + if err != nil { + if errors.Is(err, syscall.ERROR_FILE_NOT_FOUND) { + t.Skip("Perflib key not found; skipping") + } + t.Fatal(err) + } + defer k.Close() + + // Modification time of Perflib keys is known to be set to + // Filetime's zero value: get stats and check. + stats, err := k.Stat() + if err != nil { + t.Fatal(err) + } + // First verify input is zero (assume ModTimeZero uses it directly). + if !stats.ModTimeZero() { + t.Error("Modification time of Perflib key should be zero") + } + // Then check ModTime directly thus conversion implicitly. + modTime := stats.ModTime() + if !modTime.Equal(time.Date(1601, time.January, 1, 0, 0, 0, 0, time.UTC)) { + t.Errorf("ModTime should be 1601-01-01, but is %v", modTime) + } +}