diff --git a/.changelog/288.txt b/.changelog/288.txt new file mode 100644 index 0000000..718220a --- /dev/null +++ b/.changelog/288.txt @@ -0,0 +1,3 @@ +```release-note:bug +windows: Resolve kernel image path via %SystemRoot% so KernelVersion works on hosts whose system drive is not C:\. +``` diff --git a/providers/windows/kernel_windows.go b/providers/windows/kernel_windows.go index c295c79..983a220 100644 --- a/providers/windows/kernel_windows.go +++ b/providers/windows/kernel_windows.go @@ -18,13 +18,56 @@ package windows import ( + "os" + "path/filepath" + windows "github.com/elastic/go-windows" + "golang.org/x/sys/windows/registry" ) -const windowsKernelExe = `C:\Windows\System32\ntoskrnl.exe` +// fallbackSystemRoot is the last-resort default when the registry query and +// both environment variables are unavailable. +const fallbackSystemRoot = `C:\Windows` + +// systemRootFromRegistry reads the SystemRoot value from +// HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion, which reflects the +// actual Windows directory regardless of the process environment. Returns "" +// on any error so the caller can fall back gracefully. +func systemRootFromRegistry() string { + k, err := registry.OpenKey(registry.LOCAL_MACHINE, + `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, + registry.READ|registry.WOW64_64KEY) + if err != nil { + return "" + } + defer k.Close() + val, _, err := k.GetStringValue("SystemRoot") + if err != nil { + return "" + } + return val +} + +// kernelExePath returns the absolute path to the running kernel image. +// It prefers the registry (immune to a stripped process environment), then +// falls back to %SystemRoot% / %WINDIR%, then to the hardcoded default. +// See #287. +func kernelExePath() string { + root := systemRootFromRegistry() + if root == "" { + root = os.Getenv("SystemRoot") + } + if root == "" { + root = os.Getenv("WINDIR") + } + if root == "" { + root = fallbackSystemRoot + } + return filepath.Join(root, "System32", "ntoskrnl.exe") +} func KernelVersion() (string, error) { - versionData, err := windows.GetFileVersionInfo(windowsKernelExe) + versionData, err := windows.GetFileVersionInfo(kernelExePath()) if err != nil { return "", err } diff --git a/providers/windows/kernel_windows_test.go b/providers/windows/kernel_windows_test.go new file mode 100644 index 0000000..cd953f1 --- /dev/null +++ b/providers/windows/kernel_windows_test.go @@ -0,0 +1,95 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package windows + +import ( + "path/filepath" + "testing" +) + +// resolveKernelExePath is like kernelExePath but accepts an injected +// systemRoot string so tests can exercise the env-var fallback chain +// without touching the registry. +func resolveKernelExePath(regRoot, systemRoot, windir string) string { + root := regRoot + if root == "" { + root = systemRoot + } + if root == "" { + root = windir + } + if root == "" { + root = fallbackSystemRoot + } + return filepath.Join(root, "System32", "ntoskrnl.exe") +} + +func TestKernelExePath(t *testing.T) { + tests := []struct { + name string + regRoot string // simulated registry value (empty = registry miss) + envRoot string // %SystemRoot% + windir string // %WINDIR% + want string + }{ + { + // Registry wins even when env vars differ -- the primary path. + name: "registry value used when present", + regRoot: `W:\Windows`, + envRoot: `C:\Windows`, + want: `W:\Windows\System32\ntoskrnl.exe`, + }, + { + // Regression for #287: registry absent, non-default drive via env. + name: "SystemRoot env fallback", + envRoot: `W:\Windows`, + want: `W:\Windows\System32\ntoskrnl.exe`, + }, + { + name: "WINDIR fallback when SystemRoot empty", + windir: `D:\WINNT`, + want: `D:\WINNT\System32\ntoskrnl.exe`, + }, + { + name: "hardcoded fallback when all sources absent", + want: `C:\Windows\System32\ntoskrnl.exe`, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + got := resolveKernelExePath(tc.regRoot, tc.envRoot, tc.windir) + if got != tc.want { + t.Fatalf("resolveKernelExePath() = %q, want %q", got, tc.want) + } + }) + } +} + +// TestKernelExePathLive checks that the live kernelExePath (registry + env) +// returns a non-empty path ending in ntoskrnl.exe. +func TestKernelExePathLive(t *testing.T) { + p := kernelExePath() + if p == "" { + t.Fatal("kernelExePath() returned empty string") + } + if filepath.Base(p) != "ntoskrnl.exe" { + t.Fatalf("kernelExePath() = %q, want path ending in ntoskrnl.exe", p) + } +}