From 9fe982205171c8d577121d200841ba998854b689 Mon Sep 17 00:00:00 2001 From: PoloniumRain Date: Wed, 28 Jan 2026 16:30:44 +0000 Subject: [PATCH] Correctly report all Windows x86/x64 OSes in System Information --- frontend/drivers/platform_win32.c | 359 +++++++++++++++++++++--------- 1 file changed, 248 insertions(+), 111 deletions(-) diff --git a/frontend/drivers/platform_win32.c b/frontend/drivers/platform_win32.c index 7459704c9848..a5219f0587e2 100644 --- a/frontend/drivers/platform_win32.c +++ b/frontend/drivers/platform_win32.c @@ -57,6 +57,11 @@ #include "platform_win32.h" +/* Only needed for MSVC 2005/2010 */ +#ifdef _MSC_VER +#pragma comment(lib, "advapi32.lib") +#endif + #ifdef HAVE_SAPI #define COBJMACROS #include @@ -272,33 +277,90 @@ static void gfx_set_dwm(void) g_plat_win32_flags |= PLAT_WIN32_FLAG_DWM_COMPOSITION_DISABLED; } +/* Windows OS detection. + * + * Registry helper. Use RegOpenKeyExA and RegQueryValueExA for wide + * toolchain support, by reading registry strings without newer APIs. + * Always NUL-terminate the output buffer for safe string use. */ +static bool win32_reg_get_string_value( + HKEY root, const char *subkey, const char *value_name, + char *out, size_t out_size) +{ + HKEY hKey = NULL; + DWORD type = 0; + DWORD size = 0; + LONG rc = 0; + + if (!out || out_size == 0) + return false; + + out[0] = '\0'; + + rc = RegOpenKeyExA(root, subkey, 0, KEY_QUERY_VALUE, &hKey); + if (rc != ERROR_SUCCESS) + return false; + + size = (DWORD)out_size; + + rc = RegQueryValueExA(hKey, value_name, NULL, &type, (LPBYTE)out, &size); + RegCloseKey(hKey); + + if (rc == ERROR_SUCCESS && (type == REG_SZ || type == REG_EXPAND_SZ) && out[0] != '\0') + { + out[out_size - 1] = '\0'; + return true; + } + + out[0] = '\0'; + return false; +} + +/* Some Windows releases cannot be identified by NT or build number alone. + * XP x64, Server 2003, and 2003 R2 - All are NT 5.2 and build number 3790. + * Vista, 7, Server 2008, and 2008 R2 - Overlapping NT 6.0/6.1 and/or build. + * Because of this, these OSes need extra checks: read the Windows registry + * ProductType: "WinNT" is client (XP x64/Vista/7), otherwise it's server. */ +static bool win32_is_server_from_registry(void) +{ + static int cached = -1; + + if (cached < 0) + { + char product_type[32] = {0}; + + cached = 0; + + if (win32_reg_get_string_value(HKEY_LOCAL_MACHINE, + "SYSTEM\\CurrentControlSet\\Control\\ProductOptions", + "ProductType", product_type, sizeof(product_type))) + { + if (!string_is_equal(product_type, "WinNT")) + cached = 1; + } + } + + return cached == 1; +} + static size_t frontend_win32_get_os(char *s, size_t len, int *major, int *minor) { - size_t _len = 0; - char build_str[11] = {0}; - bool server = false; - const char *arch = ""; + size_t _len = 0; + char build_str[16] = {0}; + bool server = false; + const char *arch = ""; -#if defined(_WIN32_WINNT) && _WIN32_WINNT >= 0x0500 +#if 1 /* Windows 2000 and later */ - SYSTEM_INFO si = {{0}}; - OSVERSIONINFOEX vi = {0}; -#ifndef _MSC_VER - /* Vista and later, MSYS2/MINGW64 build */ - const char win_ver_reg_key[] = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion"; - const DWORD reg_read_flags = RRF_RT_REG_SZ; /* Only read strings (REG_SZ) */ - const int ProductName_2nd_digit = 9; /* second digit in the string 'Windows 10' */ - char str_ProductName[64] = {0}; - char str_DisplayVersion[64] = {0}; - char str_LCUVer[64] = {0}; - DWORD key_type = 0; /* null pointer */ - DWORD data_size = 0; - long reg_read_result; - bool read_success = TRUE; - /* end Vista and later; still within Windows 2000 and later block */ -#endif + SYSTEM_INFO si = {{0}}; + OSVERSIONINFOEX vi = {0}; + bool have_version = false; + + /* Feature version (Windows 10+) */ + char display_version[64] = {0}; + char release_id[32] = {0}; vi.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX); + vi.szCSDVersion[0] = '\0'; GetSystemInfo(&si); switch (si.wProcessorArchitecture) @@ -316,72 +378,46 @@ static size_t frontend_win32_get_os(char *s, size_t len, int *major, int *minor) break; } -#ifndef _MSC_VER - /* Vista and later, MSYS2/MINGW64 build - * Check for Win11 by looking for a specific Registry value. - * The behavior of GetVersionEx is changed under Win11 and no longer provides - * relevant data. If the specific Registry value is present, read version data - * directly from registry and skip remainder of function. - * Each read is paired for string values; the first gets the size of the - * string (read into data_size); the second passes data_size back as an - * argument and reads the actual string. */ - reg_read_result = RegGetValue(HKEY_LOCAL_MACHINE, win_ver_reg_key, "LCUVer", - reg_read_flags, &key_type, 0, &data_size); - - if (reg_read_result == ERROR_SUCCESS) + /* Prefer 'RtlGetVersion' over 'GetVersionEx' (unaffected by manifest). + * Windows 8.1 onwards will intentionally report an older OS version with + * GetVersionEx if the application does not declare support for the OS + * in the manifest, which can vary by toolchain/compiler. So instead use + * RtlGetVersion, available from NT 5.0 (Windows 2000+). GetVersionEx is + * used solely on Win9x/pre-NT 5.0, and retained as a fallback on 5.0+ */ { - if (RegGetValue(HKEY_LOCAL_MACHINE, win_ver_reg_key, "LCUVer", - reg_read_flags, &key_type, str_LCUVer, &data_size) != ERROR_SUCCESS) - read_success = FALSE; - - if (RegGetValue(HKEY_LOCAL_MACHINE, win_ver_reg_key, "ProductName", - reg_read_flags, &key_type, 0, &data_size) != ERROR_SUCCESS) - read_success = FALSE; - - if (RegGetValue(HKEY_LOCAL_MACHINE, win_ver_reg_key, "ProductName", - reg_read_flags, &key_type, str_ProductName, &data_size) != ERROR_SUCCESS) - read_success = FALSE; + typedef LONG (WINAPI *RtlGetVersionFn)(OSVERSIONINFOEXW*); + HMODULE ntdll_handle = GetModuleHandleA("ntdll.dll"); + RtlGetVersionFn rtl_version = ntdll_handle ? + (RtlGetVersionFn)GetProcAddress(ntdll_handle, "RtlGetVersion") : NULL; - if (RegGetValue(HKEY_LOCAL_MACHINE, win_ver_reg_key, "DisplayVersion", - reg_read_flags, &key_type, 0, &data_size) != ERROR_SUCCESS) - read_success = FALSE; - - if (RegGetValue(HKEY_LOCAL_MACHINE, win_ver_reg_key, "DisplayVersion", - reg_read_flags, &key_type, str_DisplayVersion, &data_size) != ERROR_SUCCESS) - read_success = FALSE; - - if (read_success) + if (rtl_version) { - str_ProductName[ProductName_2nd_digit] = '1'; - /* Even the version in the Registry still says Windows 10 and requires - * string manipulation. */ + OSVERSIONINFOEXW vi_w; + memset(&vi_w, 0, sizeof(vi_w)); + vi_w.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEXW); - _len = strlcpy(s, str_ProductName, len); - - if (!string_is_empty(arch)) + if (rtl_version(&vi_w) == 0) { - _len += strlcpy(s + _len, " ", len - _len); - _len += strlcpy(s + _len, arch, len - _len); + vi.dwMajorVersion = vi_w.dwMajorVersion; + vi.dwMinorVersion = vi_w.dwMinorVersion; + vi.dwBuildNumber = vi_w.dwBuildNumber; + vi.dwPlatformId = vi_w.dwPlatformId; + vi.wProductType = vi_w.wProductType; + vi.wSuiteMask = vi_w.wSuiteMask; + + if (vi_w.szCSDVersion[0]) + WideCharToMultiByte(CP_UTF8, 0, vi_w.szCSDVersion, -1, + vi.szCSDVersion, sizeof(vi.szCSDVersion), NULL, NULL); + + have_version = true; } - _len = strlcpy(s + _len, " ", len - _len); - _len = strlcpy(s + _len, str_DisplayVersion, len - _len); - _len = strlcpy(s + _len, " (", len - _len); - _len = strlcpy(s + _len, str_LCUVer, len - _len); - _len = strlcpy(s + _len, ")", len - _len); - - *major = 10; - *minor = 0; - - return _len; } } - /* End registry-check-and-read code; still within 2000-and-later block */ -#endif - /* GetVersionEx call changed in Win2K and later */ - GetVersionEx((OSVERSIONINFO*)&vi); + if (!have_version) + GetVersionEx((OSVERSIONINFO*)&vi); - server = vi.wProductType != VER_NT_WORKSTATION; + server = (vi.wProductType != VER_NT_WORKSTATION); #else OSVERSIONINFO vi = {0}; @@ -392,62 +428,117 @@ static size_t frontend_win32_get_os(char *s, size_t len, int *major, int *minor) #endif if (major) - *major = vi.dwMajorVersion; - + *major = (int)vi.dwMajorVersion; if (minor) - *minor = vi.dwMinorVersion; + *minor = (int)vi.dwMinorVersion; if (vi.dwMajorVersion == 4 && vi.dwMinorVersion == 0) - snprintf(build_str, sizeof(build_str), "%lu", (DWORD)(LOWORD(vi.dwBuildNumber))); /* Windows 95 build number is in the low-order word only */ + snprintf(build_str, sizeof(build_str), "%lu", + (DWORD)(LOWORD(vi.dwBuildNumber))); /* Windows 95 build number is in the low-order word only */ else - snprintf(build_str, sizeof(build_str), "%lu", vi.dwBuildNumber); + snprintf(build_str, sizeof(build_str), "%lu", (DWORD)vi.dwBuildNumber); + + /* Read Windows 10/11/Server feature version (e.g. "25H2") from registry. + * Used solely for display; not relied upon for version detection. Note: + * Windows Insider Preview builds may show channel labels (e.g. "Dev") */ + if (vi.dwMajorVersion >= 10) + { + const char win_ver_reg_key[] = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion"; + + if (!win32_reg_get_string_value(HKEY_LOCAL_MACHINE, win_ver_reg_key, + "DisplayVersion", display_version, sizeof(display_version))) + display_version[0] = '\0'; + + /* ReleaseId is for older Win10 versions (e.g. 1511, 1607) */ + if (!win32_reg_get_string_value(HKEY_LOCAL_MACHINE, win_ver_reg_key, + "ReleaseId", release_id, sizeof(release_id))) + release_id[0] = '\0'; + + /* The first Win10 release doesn't provide DisplayVersion/ReleaseId, + * so use build number to insert version label */ + if (!server && vi.dwMajorVersion == 10 && vi.dwBuildNumber == 10240 + && string_is_empty(display_version) && string_is_empty(release_id)) + { + strlcpy(release_id, "1507", sizeof(release_id)); + } + } + /* Detect Windows version from build number, NT version, or platform ID. + * Windows 10, 11, and modern Windows Server all report NT 10.0, so each + * is identified by using build numbers. Older NT-based Windows versions + * mostly have unique major/minor versions; a few share versions between + * client/server and require extra checks (e.g. ProductType, SM_SERVERR2). + * Non-NT Windows (95/98/ME) are identified via the platform ID. */ switch (vi.dwMajorVersion) { case 10: - if (atoi(build_str) >= 21996) - _len = strlcpy(s, "Windows 11", len); - else if (server) - _len = strlcpy(s, "Windows Server 2016", len); + if (server) + { + if (vi.dwBuildNumber >= 26040) + _len = strlcpy(s, "Windows Server 2025", len); + else if (vi.dwBuildNumber >= 20201) + _len = strlcpy(s, "Windows Server 2022", len); + else if (vi.dwBuildNumber >= 17623) + _len = strlcpy(s, "Windows Server 2019", len); + /* Early Server 2016 preview builds shared build numbers with + * Windows 10 previews, so 10074 is used as a safe cutoff here */ + else if (vi.dwBuildNumber >= 10074) + _len = strlcpy(s, "Windows Server 2016", len); + else + _len = snprintf(s, len, "Windows Server NT kernel %lu.%lu", + (unsigned long)vi.dwMajorVersion, (unsigned long)vi.dwMinorVersion); + } else - _len = strlcpy(s, "Windows 10", len); + { + /* Detect Windows 11 starting from an early leaked preview build */ + if (vi.dwBuildNumber >= 21996) + _len = strlcpy(s, "Windows 11", len); + /* Detect Windows 10 from the first NT 10.0-based preview build */ + else if (vi.dwBuildNumber >= 9888) + _len = strlcpy(s, "Windows 10", len); + else + _len = snprintf(s, len, "Windows NT kernel %lu.%lu", + (unsigned long)vi.dwMajorVersion, (unsigned long)vi.dwMinorVersion); + } break; + case 6: switch (vi.dwMinorVersion) { case 3: - if (server) - _len = strlcpy(s, "Windows Server 2012 R2", len); - else - _len = strlcpy(s, "Windows 8.1", len); + _len = strlcpy(s, server ? "Windows Server 2012 R2" : "Windows 8.1", len); break; case 2: - if (server) - _len = strlcpy(s, "Windows Server 2012", len); - else - _len = strlcpy(s, "Windows 8", len); + _len = strlcpy(s, server ? "Windows Server 2012" : "Windows 8", len); break; case 1: - if (server) - _len = strlcpy(s, "Windows Server 2008 R2", len); - else - _len = strlcpy(s, "Windows 7", len); + { + bool is_server = server; + if (!is_server) + is_server = win32_is_server_from_registry(); + _len = strlcpy(s, is_server ? "Windows Server 2008 R2" : "Windows 7", len); + } break; case 0: - if (server) - _len = strlcpy(s, "Windows Server 2008", len); - else - _len = strlcpy(s, "Windows Vista", len); + { + bool is_server = server; + if (!is_server) + is_server = win32_is_server_from_registry(); + _len = strlcpy(s, is_server ? "Windows Server 2008" : "Windows Vista", len); + } break; default: + _len = snprintf(s, len, "Windows NT kernel %lu.%lu", + (unsigned long)vi.dwMajorVersion, (unsigned long)vi.dwMinorVersion); break; } break; + case 5: switch (vi.dwMinorVersion) { case 2: - if (server) + if (server || win32_is_server_from_registry()) { _len = strlcpy(s, "Windows Server 2003", len); if (GetSystemMetrics(SM_SERVERR2)) @@ -455,9 +546,9 @@ static size_t frontend_win32_get_os(char *s, size_t len, int *major, int *minor) } else { - /* Yes, XP Pro x64 is a higher version number than XP x86 */ - if (string_is_equal(arch, "x64")) - _len = strlcpy(s, "Windows XP", len); + /* XP "x64 Edition" is NT 5.2 (XP is 5.1) and only ever had one + * edition, making it safe to use the full product name here */ + _len = strlcpy(s, "Windows XP Professional x64 Edition", len); } break; case 1: @@ -466,8 +557,13 @@ static size_t frontend_win32_get_os(char *s, size_t len, int *major, int *minor) case 0: _len = strlcpy(s, "Windows 2000", len); break; + default: + _len = snprintf(s, len, "Windows NT kernel %lu.%lu", + (unsigned long)vi.dwMajorVersion, (unsigned long)vi.dwMinorVersion); + break; } break; + case 4: switch (vi.dwMinorVersion) { @@ -480,25 +576,65 @@ static size_t frontend_win32_get_os(char *s, size_t len, int *major, int *minor) _len = strlcpy(s, "Unknown", len); break; case 90: - _len = strlcpy(s, "Windows ME", len); + /* Apparently it's not "ME". Official naming always uses "Me" */ + _len = strlcpy(s, "Windows Me", len); break; case 10: + { + DWORD win98_build; + win98_build = (DWORD)(LOWORD(vi.dwBuildNumber)); _len = strlcpy(s, "Windows 98", len); + /* 98/98 SE are both Win9x 4.10, so detect SE by build number */ + if (win98_build >= 2222) + _len += strlcpy(s + _len, " Second Edition", len - _len); + break; + } + default: + if (vi.dwPlatformId == VER_PLATFORM_WIN32_NT) + _len = snprintf(s, len, "Windows NT kernel %lu.%lu", + (unsigned long)vi.dwMajorVersion, (unsigned long)vi.dwMinorVersion); + else + _len = snprintf(s, len, "Windows 9x version %lu.%lu", + (unsigned long)vi.dwMajorVersion, (unsigned long)vi.dwMinorVersion); break; } break; + default: - _len = snprintf(s, len, "Windows %i.%i", *major, *minor); + /* Fallback for completely unknown or future Windows versions */ + _len = snprintf(s, len, "Windows NT kernel %lu.%lu", + (unsigned long)vi.dwMajorVersion, (unsigned long)vi.dwMinorVersion); break; } - if (!string_is_empty(arch)) +/* OS display formatting */ + if (vi.dwMajorVersion >= 10) + { + if (!string_is_empty(display_version)) + { + _len += strlcpy(s + _len, " (", len - _len); + _len += strlcpy(s + _len, display_version, len - _len); + _len += strlcpy(s + _len, ")", len - _len); + } + else if (!string_is_empty(release_id)) + { + _len += strlcpy(s + _len, " (", len - _len); + _len += strlcpy(s + _len, release_id, len - _len); + _len += strlcpy(s + _len, ")", len - _len); + } + } + /* Hide x86/x64 for XP x64 ("x64" is already shown in the OS name) + * and OSes older than XP x86, since they are all x86 only */ + if (!string_is_empty(arch) && + (vi.dwMajorVersion > 5 || + (vi.dwMajorVersion == 5 && vi.dwMinorVersion >= 1)) && + !(vi.dwMajorVersion == 5 && vi.dwMinorVersion == 2)) { _len += strlcpy(s + _len, " ", len - _len); _len += strlcpy(s + _len, arch, len - _len); } - _len += strlcpy(s + _len, " Build ", len - _len); + _len += strlcpy(s + _len, " - Build ", len - _len); _len += strlcpy(s + _len, build_str, len - _len); if (!string_is_empty(vi.szCSDVersion)) @@ -506,6 +642,7 @@ static size_t frontend_win32_get_os(char *s, size_t len, int *major, int *minor) _len += strlcpy(s + _len, " ", len - _len); strlcpy(s + _len, vi.szCSDVersion, len - _len); } + return _len; }