summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDavid Lönnhager <david.l@mullvad.net>2025-08-06 14:08:25 +0200
committerDavid Lönnhager <david.l@mullvad.net>2025-08-07 13:48:29 +0200
commit35fa899e46316caf7669eee8bab7eb2fb6e84107 (patch)
treeda2c9c9161542e7619227c05c1fabdbf5fbdebae
parent26fddca7799ad01425458262d5ca774ded1ce96f (diff)
downloadmullvadvpn-35fa899e46316caf7669eee8bab7eb2fb6e84107.tar.xz
mullvadvpn-35fa899e46316caf7669eee8bab7eb2fb6e84107.zip
Infer Windows version from ntoskrnl image in installer
-rw-r--r--dist-assets/windows/installer.nsh17
-rw-r--r--mullvad-nsis/include/mullvad-nsis.h13
-rw-r--r--mullvad-nsis/src/lib.rs31
-rw-r--r--talpid-platform-metadata/Cargo.toml4
-rw-r--r--talpid-platform-metadata/src/windows.rs152
-rw-r--r--windows/nsis-plugins/src/cleanup/cleanup.cpp3
-rw-r--r--windows/nsis-plugins/src/log/log.cpp38
-rw-r--r--windows/nsis-plugins/src/log/log.def1
8 files changed, 235 insertions, 24 deletions
diff --git a/dist-assets/windows/installer.nsh b/dist-assets/windows/installer.nsh
index f80d379d31..c6fcf161d1 100644
--- a/dist-assets/windows/installer.nsh
+++ b/dist-assets/windows/installer.nsh
@@ -689,10 +689,19 @@ ManifestSupportedOS "{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"
Push $0
- ${IfNot} ${AtLeastWin10}
- MessageBox MB_ICONSTOP|MB_TOPMOST|MB_OK "Windows versions below 10 are unsupported. The last version to support Windows 7 and 8/8.1 is 2021.6."
- Abort
- ${EndIf}
+ # We do not use AtLeastWin10, because it is affected by compatibility mode. Instead, we infer
+ # the version from the kernel image.
+ log::GetWindowsMajorVersion
+ Pop $0
+
+ IntCmp $0 10 customInit_compatibleWinVer +1 customInit_compatibleWinVer
+ # Best effort only. Ignore errors
+ IntCmp $0 -1 customInit_compatibleWinVer customInit_compatibleWinVer +1
+
+ MessageBox MB_ICONSTOP|MB_TOPMOST|MB_OK "Windows versions below 10 are unsupported. The last version to support Windows 7 and 8/8.1 is 2021.6."
+ Abort
+
+ customInit_compatibleWinVer:
Var /GLOBAL NativeTarget
${If} ${IsNativeAMD64}
diff --git a/mullvad-nsis/include/mullvad-nsis.h b/mullvad-nsis/include/mullvad-nsis.h
index 3847df273b..f8870acff9 100644
--- a/mullvad-nsis/include/mullvad-nsis.h
+++ b/mullvad-nsis/include/mullvad-nsis.h
@@ -12,6 +12,13 @@ enum class Status {
Panic,
};
+/// Windows version details
+struct WindowsVer {
+ uint32_t major_version;
+ uint32_t minor_version;
+ uint32_t build_number;
+};
+
extern "C" {
/// Creates a privileged directory at the specified Windows path.
@@ -43,4 +50,10 @@ Status get_system_local_appdata(uint16_t *buffer, uintptr_t *buffer_size);
/// at least `*buffer_size` number of `u16` values. `buffer_size` must be a valid pointer.
Status get_system_version(uint16_t *buffer, uintptr_t *buffer_size);
+/// Write OS version into `version_out` when `Status::Ok` is returned.
+///
+/// # Safety
+/// `version_out` should point to a valid `WindowsVer`
+Status get_system_version_struct(WindowsVer *version_out);
+
} // extern "C"
diff --git a/mullvad-nsis/src/lib.rs b/mullvad-nsis/src/lib.rs
index 5aed1389c0..6bb7472eff 100644
--- a/mullvad-nsis/src/lib.rs
+++ b/mullvad-nsis/src/lib.rs
@@ -142,6 +142,37 @@ pub unsafe extern "C" fn get_system_version(buffer: *mut u16, buffer_size: *mut
})
}
+/// Windows version details
+#[repr(C)]
+pub struct WindowsVer {
+ major_version: u32,
+ minor_version: u32,
+ build_number: u32,
+}
+
+/// Write OS version into `version_out` when `Status::Ok` is returned.
+///
+/// # Safety
+/// `version_out` should point to a valid `WindowsVer`
+#[unsafe(no_mangle)]
+pub unsafe extern "C" fn get_system_version_struct(version_out: *mut WindowsVer) -> Status {
+ use talpid_platform_metadata::WindowsVersion;
+ catch_and_log_unwind(|| {
+ // Try to retrieve the version based on the kernel image. Use normal method as fallback.
+ let winver = WindowsVersion::from_ntoskrnl()
+ .or_else(|_| WindowsVersion::new())
+ .unwrap();
+ let c_ver = WindowsVer {
+ major_version: winver.major_version(),
+ minor_version: winver.minor_version(),
+ build_number: winver.build_number(),
+ };
+ // SAFETY: `version_out` is a valid `WindowsVer` if the caller upholds the contract.
+ unsafe { ptr::write(version_out, c_ver) };
+ Status::Ok
+ })
+}
+
fn catch_and_log_unwind(func: impl FnOnce() -> Status + UnwindSafe) -> Status {
match std::panic::catch_unwind(func) {
Ok(status) => status,
diff --git a/talpid-platform-metadata/Cargo.toml b/talpid-platform-metadata/Cargo.toml
index 9eb58f4a55..66db6c4285 100644
--- a/talpid-platform-metadata/Cargo.toml
+++ b/talpid-platform-metadata/Cargo.toml
@@ -22,6 +22,10 @@ talpid-dbus = { path = "../talpid-dbus", optional = true }
workspace = true
features = [
"Win32_Foundation",
+ "Win32",
+ "Win32_Storage",
+ "Win32_Storage_FileSystem",
+ "Win32_System_Diagnostics_Debug",
"Win32_System_LibraryLoader",
"Win32_System_SystemInformation",
"Win32_System_SystemServices",
diff --git a/talpid-platform-metadata/src/windows.rs b/talpid-platform-metadata/src/windows.rs
index ec2b7b82be..9a23cc21c4 100644
--- a/talpid-platform-metadata/src/windows.rs
+++ b/talpid-platform-metadata/src/windows.rs
@@ -1,14 +1,23 @@
use std::{
- ffi::OsString,
+ ffi::{OsStr, OsString},
io, iter,
mem::{self, MaybeUninit},
- os::windows::ffi::OsStrExt,
+ os::{
+ raw::c_void,
+ windows::ffi::{OsStrExt, OsStringExt},
+ },
+ path::PathBuf,
+ ptr,
};
use windows_sys::Win32::{
- Foundation::{NTSTATUS, STATUS_SUCCESS},
+ Foundation::{MAX_PATH, NTSTATUS, STATUS_SUCCESS},
+ Storage::FileSystem::{
+ GetFileVersionInfoSizeW, GetFileVersionInfoW, VS_FFI_SIGNATURE, VS_FIXEDFILEINFO,
+ VerQueryValueW,
+ },
System::{
LibraryLoader::{GetModuleHandleW, GetProcAddress},
- SystemInformation::OSVERSIONINFOEXW,
+ SystemInformation::{GetSystemDirectoryW, OSVERSIONINFOEXW},
SystemServices::VER_NT_WORKSTATION,
},
};
@@ -41,16 +50,22 @@ pub fn extra_metadata() -> impl Iterator<Item = (String, String)> {
}
pub struct WindowsVersion {
- inner: RTL_OSVERSIONINFOEXW,
+ major: u32,
+ minor: u32,
+ build: u32,
+ product_type: ProductType,
+}
+
+#[derive(PartialEq)]
+enum ProductType {
+ Unknown,
+ Workstation,
+ Server,
}
impl WindowsVersion {
pub fn new() -> Result<WindowsVersion, io::Error> {
- let module_name: Vec<u16> = OsString::from("ntdll")
- .as_os_str()
- .encode_wide()
- .chain(iter::once(0u16))
- .collect();
+ let module_name = to_wide("ntdll");
// SAFETY: module_name is a valid UTF-16/WTF-16 null-terminated string.
let ntdll = unsafe { GetModuleHandleW(module_name.as_ptr()) };
@@ -87,15 +102,35 @@ impl WindowsVersion {
);
Ok(WindowsVersion {
- inner: version_info,
+ major: version_info.dwMajorVersion,
+ minor: version_info.dwMinorVersion,
+ build: version_info.dwBuildNumber,
+ product_type: match u32::from(version_info.wProductType) {
+ // `wProductType != VER_NT_WORKSTATION` implies that OS is Windows Server
+ // https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ns-wdm-_osversioninfoexw
+ VER_NT_WORKSTATION => ProductType::Workstation,
+ _ => ProductType::Server,
+ },
+ })
+ }
+
+ /// Extract Windows version information from the kernel image, which is unaffected by compatibility
+ /// mode. Note that this does not infer whether we are running Windows Server or a normal version.
+ pub fn from_ntoskrnl() -> io::Result<Self> {
+ let (major, minor, build) = ntoskrnl_version()?;
+
+ Ok(Self {
+ major,
+ minor,
+ build,
+ // NOTE: We do not have the product type here
+ product_type: ProductType::Unknown,
})
}
pub fn windows_version_string(&self) -> String {
- // `wProductType != VER_NT_WORKSTATION` implies that OS is Windows Server
- // https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ns-wdm-_osversioninfoexw
- // NOTE: This does not deduce which Windows Server version is running.
- if u32::from(self.inner.wProductType) != VER_NT_WORKSTATION {
+ if self.product_type == ProductType::Server {
+ // NOTE: This does not deduce which Windows Server version is running.
return "Server".to_owned();
}
@@ -125,23 +160,104 @@ impl WindowsVersion {
}
pub fn major_version(&self) -> u32 {
- self.inner.dwMajorVersion
+ self.major
}
pub fn minor_version(&self) -> u32 {
- self.inner.dwMinorVersion
+ self.minor
}
pub fn build_number(&self) -> u32 {
- self.inner.dwBuildNumber
+ self.build
+ }
+}
+
+fn ntoskrnl_version() -> io::Result<(u32, u32, u32)> {
+ let ntoskrnl_path = get_system_dir()?.join("ntoskrnl.exe");
+ let wide_path = to_wide(ntoskrnl_path);
+ let mut handle = 0u32;
+
+ // SAFETY: We have a valid string and `handle` pointer
+ let size = unsafe { GetFileVersionInfoSizeW(wide_path.as_ptr(), &mut handle) };
+ if size == 0 {
+ return Err(io::Error::last_os_error());
+ }
+
+ let mut buffer = vec![0u8; size as usize];
+ // SAFETY: `buffer` contains enough space to store the result
+ let status =
+ unsafe { GetFileVersionInfoW(wide_path.as_ptr(), 0, size, buffer.as_mut_ptr() as *mut _) };
+
+ if status == 0 {
+ return Err(io::Error::last_os_error());
+ }
+
+ let mut lp_buffer: *mut c_void = ptr::null_mut();
+ let mut len = 0u32;
+
+ let sub_block = to_wide(r"\");
+ // SAFETY: `buffer` points to a valid version-info resource
+ let success = unsafe {
+ VerQueryValueW(
+ buffer.as_ptr() as *const _,
+ sub_block.as_ptr(),
+ &mut lp_buffer,
+ &mut len,
+ )
+ };
+
+ if success == 0 || lp_buffer.is_null() {
+ return Err(io::Error::last_os_error());
+ }
+
+ // SAFETY: `lp_buffer` points to a valid `VS_FIXEDFILEINFO`
+ let info = unsafe { &*(lp_buffer as *const VS_FIXEDFILEINFO) };
+ if info.dwSignature != VS_FFI_SIGNATURE as u32 {
+ return Err(io::Error::other("Invalid version info signature"));
+ }
+
+ let major = info.dwProductVersionMS >> 16;
+ let minor = info.dwProductVersionMS & 0xFFFF;
+ let build = info.dwProductVersionLS >> 16;
+
+ Ok((major, minor, build))
+}
+
+fn get_system_dir() -> io::Result<PathBuf> {
+ let mut sysdir = [0u16; MAX_PATH as usize + 1];
+ // SAFETY: `sysdir` points to a valid buffer
+ let len = unsafe { GetSystemDirectoryW(sysdir.as_mut_ptr(), (sysdir.len() - 1) as u32) };
+ if len == 0 {
+ return Err(io::Error::last_os_error());
}
+ Ok(PathBuf::from(OsString::from_wide(
+ &sysdir[0..(len as usize)],
+ )))
+}
+
+/// Return a null-terminated UTF16 string
+fn to_wide(s: impl AsRef<OsStr>) -> Vec<u16> {
+ s.as_ref().encode_wide().chain(iter::once(0u16)).collect()
}
#[cfg(test)]
mod test {
use super::*;
+
#[test]
fn test_windows_version() {
WindowsVersion::new().unwrap();
}
+
+ #[test]
+ fn test_ntoskrnl_version() {
+ let winver = WindowsVersion::new().unwrap();
+ let nt_winver = WindowsVersion::from_ntoskrnl().unwrap();
+
+ assert_eq!(winver.major, nt_winver.major);
+ assert_eq!(winver.minor, nt_winver.minor);
+ assert_eq!(winver.build, nt_winver.build);
+
+ // NOTE: We do not know the product type for `nt_winver`
+ }
}
diff --git a/windows/nsis-plugins/src/cleanup/cleanup.cpp b/windows/nsis-plugins/src/cleanup/cleanup.cpp
index ca943934a7..b52c269577 100644
--- a/windows/nsis-plugins/src/cleanup/cleanup.cpp
+++ b/windows/nsis-plugins/src/cleanup/cleanup.cpp
@@ -7,6 +7,9 @@
#include <functional>
#include <vector>
+// NOTE: Linker refuses to find the library unless specified here
+#pragma comment(lib, "version.lib")
+
void __declspec(dllexport) NSISCALL RemoveLogsAndCache
(
HWND hwndParent,
diff --git a/windows/nsis-plugins/src/log/log.cpp b/windows/nsis-plugins/src/log/log.cpp
index d1e37d7669..64eabedf8d 100644
--- a/windows/nsis-plugins/src/log/log.cpp
+++ b/windows/nsis-plugins/src/log/log.cpp
@@ -90,7 +90,7 @@ std::vector<std::wstring> BlockToRows(const std::wstring &textBlock)
return common::string::Tokenize(textBlock, L"\r\n");
}
-std::wstring GetWindowsVersion()
+std::wstring GetWindowsVersionString()
{
std::vector<uint16_t> version(256);
size_t bufferSize = version.size();
@@ -287,7 +287,7 @@ void __declspec(dllexport) NSISCALL LogWindowsVersion
try
{
std::wstringstream version;
- version << L"Windows version: " << GetWindowsVersion();
+ version << L"Windows version: " << GetWindowsVersionString();
g_logger->log(version.str());
}
catch (std::exception &err)
@@ -306,6 +306,40 @@ void __declspec(dllexport) NSISCALL LogWindowsVersion
}
//
+// GetWindowsMajorVersion
+//
+// Returns the current Windows major version on the stack. -1 on error.
+//
+void __declspec(dllexport) NSISCALL GetWindowsMajorVersion
+(
+ HWND hwndParent,
+ int string_size,
+ LPTSTR variables,
+ stack_t **stacktop,
+ extra_parameters *extra,
+ ...
+)
+{
+ EXDLL_INIT();
+
+ WindowsVer ver = { 0 };
+
+ if (get_system_version_struct(&ver) == Status::Ok)
+ {
+ pushint(ver.major_version);
+ return;
+ }
+
+
+ if (nullptr != g_logger)
+ {
+ g_logger->log(L"Windows version: Failed to determine version");
+ }
+
+ pushint(-1);
+}
+
+//
// PluginLog
//
// Writes a message to the log file.
diff --git a/windows/nsis-plugins/src/log/log.def b/windows/nsis-plugins/src/log/log.def
index d51b04ab7b..f8bafa4a57 100644
--- a/windows/nsis-plugins/src/log/log.def
+++ b/windows/nsis-plugins/src/log/log.def
@@ -6,6 +6,7 @@ SetLogTarget
Log
LogWithDetails
LogWindowsVersion
+GetWindowsMajorVersion
PluginLog
PluginLogWithDetails