From 21d12417460eb822c5d15c932acb5cd6ea3f5933 Mon Sep 17 00:00:00 2001 From: SAY-5 Date: Thu, 21 May 2026 00:56:28 -0700 Subject: [PATCH 1/3] fix(windows): resolve kernel path via %SystemRoot% KernelVersion() called GetFileVersionInfo on a hardcoded C:\Windows\System32\ntoskrnl.exe, which fails with ERROR_FILE_NOT_FOUND on Windows hosts whose system drive is not C:\. The failure propagates through sysinfo.Host() and breaks Elastic Agent install/enroll on such hosts. Resolve the kernel path through %SystemRoot% (falling back to %WINDIR% and then the existing C:\Windows default) and add a regression test. Fixes #287 --- providers/windows/kernel_windows.go | 25 ++++++++- providers/windows/kernel_windows_test.go | 65 ++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 providers/windows/kernel_windows_test.go diff --git a/providers/windows/kernel_windows.go b/providers/windows/kernel_windows.go index c295c799..97248cbc 100644 --- a/providers/windows/kernel_windows.go +++ b/providers/windows/kernel_windows.go @@ -18,13 +18,34 @@ package windows import ( + "os" + "path/filepath" + windows "github.com/elastic/go-windows" ) -const windowsKernelExe = `C:\Windows\System32\ntoskrnl.exe` +// fallbackSystemRoot is used when neither %SystemRoot% nor %WINDIR% is set. +// In practice the Windows session manager always seeds both, but a hardened +// service environment could strip them; keep a sensible default rather than +// returning an error from a getter that historically never required one. +const fallbackSystemRoot = `C:\Windows` + +// kernelExePath returns the absolute path to the running kernel image. It +// honours %SystemRoot% (and %WINDIR% as a backwards-compatible alias) so +// hosts whose system drive is not C:\ are handled correctly. See #287. +func kernelExePath() string { + 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 00000000..ab6f0d64 --- /dev/null +++ b/providers/windows/kernel_windows_test.go @@ -0,0 +1,65 @@ +// 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 ( + "testing" +) + +func TestKernelExePath(t *testing.T) { + tests := []struct { + name string + systemRoot string + windir string + want string + }{ + { + // Regression for #287: when the system drive is not C:\ the + // computed kernel path must follow %SystemRoot% instead of the + // hardcoded C:\Windows. + name: "non-default SystemRoot drive", + systemRoot: `W:\Windows`, + want: `W:\Windows\System32\ntoskrnl.exe`, + }, + { + name: "default SystemRoot", + systemRoot: `C:\Windows`, + want: `C:\Windows\System32\ntoskrnl.exe`, + }, + { + name: "WINDIR fallback when SystemRoot empty", + windir: `D:\WINNT`, + want: `D:\WINNT\System32\ntoskrnl.exe`, + }, + { + name: "fallback when neither env var is set", + want: `C:\Windows\System32\ntoskrnl.exe`, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Setenv("SystemRoot", tc.systemRoot) + t.Setenv("WINDIR", tc.windir) + if got := kernelExePath(); got != tc.want { + t.Fatalf("kernelExePath() = %q, want %q", got, tc.want) + } + }) + } +} From 4dc4e59816b06336da6da6a00b514ccec9ce9f4b Mon Sep 17 00:00:00 2001 From: SAY-5 Date: Thu, 21 May 2026 00:56:58 -0700 Subject: [PATCH 2/3] Add changelog entry for #288 --- .changelog/288.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/288.txt diff --git a/.changelog/288.txt b/.changelog/288.txt new file mode 100644 index 00000000..718220a9 --- /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:\. +``` From 7a745fb6bda71974cee3d816f17fb5c19ca5716f Mon Sep 17 00:00:00 2001 From: Sai Asish Y Date: Tue, 26 May 2026 11:18:35 -0700 Subject: [PATCH 3/3] fix(windows): query registry for SystemRoot, fall back to env vars Signed-off-by: Sai Asish Y --- providers/windows/kernel_windows.go | 38 +++++++++++--- providers/windows/kernel_windows_test.go | 66 +++++++++++++++++------- 2 files changed, 78 insertions(+), 26 deletions(-) diff --git a/providers/windows/kernel_windows.go b/providers/windows/kernel_windows.go index 97248cbc..983a220f 100644 --- a/providers/windows/kernel_windows.go +++ b/providers/windows/kernel_windows.go @@ -22,19 +22,41 @@ import ( "path/filepath" windows "github.com/elastic/go-windows" + "golang.org/x/sys/windows/registry" ) -// fallbackSystemRoot is used when neither %SystemRoot% nor %WINDIR% is set. -// In practice the Windows session manager always seeds both, but a hardened -// service environment could strip them; keep a sensible default rather than -// returning an error from a getter that historically never required one. +// fallbackSystemRoot is the last-resort default when the registry query and +// both environment variables are unavailable. const fallbackSystemRoot = `C:\Windows` -// kernelExePath returns the absolute path to the running kernel image. It -// honours %SystemRoot% (and %WINDIR% as a backwards-compatible alias) so -// hosts whose system drive is not C:\ are handled correctly. See #287. +// 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 := os.Getenv("SystemRoot") + root := systemRootFromRegistry() + if root == "" { + root = os.Getenv("SystemRoot") + } if root == "" { root = os.Getenv("WINDIR") } diff --git a/providers/windows/kernel_windows_test.go b/providers/windows/kernel_windows_test.go index ab6f0d64..cd953f14 100644 --- a/providers/windows/kernel_windows_test.go +++ b/providers/windows/kernel_windows_test.go @@ -18,28 +18,47 @@ 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 - systemRoot string - windir string - want string + name string + regRoot string // simulated registry value (empty = registry miss) + envRoot string // %SystemRoot% + windir string // %WINDIR% + want string }{ { - // Regression for #287: when the system drive is not C:\ the - // computed kernel path must follow %SystemRoot% instead of the - // hardcoded C:\Windows. - name: "non-default SystemRoot drive", - systemRoot: `W:\Windows`, - want: `W:\Windows\System32\ntoskrnl.exe`, + // 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`, }, { - name: "default SystemRoot", - systemRoot: `C:\Windows`, - want: `C:\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", @@ -47,7 +66,7 @@ func TestKernelExePath(t *testing.T) { want: `D:\WINNT\System32\ntoskrnl.exe`, }, { - name: "fallback when neither env var is set", + name: "hardcoded fallback when all sources absent", want: `C:\Windows\System32\ntoskrnl.exe`, }, } @@ -55,11 +74,22 @@ func TestKernelExePath(t *testing.T) { for _, tc := range tests { tc := tc t.Run(tc.name, func(t *testing.T) { - t.Setenv("SystemRoot", tc.systemRoot) - t.Setenv("WINDIR", tc.windir) - if got := kernelExePath(); got != tc.want { - t.Fatalf("kernelExePath() = %q, want %q", got, tc.want) + 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) + } +}