Skip to content

Commit 7fe1038

Browse files
authored
fix(dgw): resolve WER APIs dynamically to support Windows Server 2016 (#1751)
`WerRegisterExcludedMemoryBlock` is absent from `wer.dll` on Windows Server 2016 RTM (NT 10.0.14393). The static import introduced in v2026.1.1 caused the Windows loader to refuse starting DevolutionsGateway.exe on those hosts with a "procedure entry point not found" error. Replace the static import with runtime resolution via `GetModuleHandleW` and `GetProcAddress`, cached in a `OnceLock`. The binary now starts on any Windows 10/Server 2016 build and silently skips WER dump exclusion when the API is unavailable; all other protections (guard pages, `VirtualLock`, `PAGE_READONLY`) still apply. Issue: DGW-368
1 parent 0c598f6 commit 7fe1038

3 files changed

Lines changed: 87 additions & 16 deletions

File tree

crates/secure-memory/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ libc = "0.2"
2121
version = "0.61"
2222
features = [
2323
"Win32_Foundation",
24-
"Win32_System_ErrorReporting",
24+
"Win32_System_LibraryLoader",
2525
"Win32_System_Memory",
2626
"Win32_System_SystemInformation",
2727
]

crates/secure-memory/src/lib.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ mod tests {
260260
assert_eq!(status.level(), ProtectionLevel::Unprotected);
261261
}
262262

263-
#[cfg(any(windows, target_os = "linux"))]
263+
#[cfg(target_os = "linux")]
264264
#[test]
265265
fn os_backend_is_full_protection() {
266266
let secret = ProtectedBytes::new(&mut [5u8; 32]);
@@ -270,4 +270,18 @@ mod tests {
270270
"expected Full protection on this platform"
271271
);
272272
}
273+
274+
// On Windows, `WerRegisterExcludedMemoryBlock` is absent on some Windows Server 2016
275+
// (NT 10.0.14393) builds, so `dump_excluded` is best-effort and `Full` is not
276+
// guaranteed. `VirtualLock` is also best-effort (can fail under working-set limits).
277+
// Assert only the protections that are reliably available.
278+
#[cfg(windows)]
279+
#[test]
280+
fn os_backend_is_full_protection() {
281+
let secret = ProtectedBytes::new(&mut [5u8; 32]);
282+
let st = secret.protection_status();
283+
assert!(!st.fallback_backend, "OS backend should be active");
284+
assert!(st.guard_pages, "guard pages should be active");
285+
assert!(st.write_protected, "data page should be write-protected");
286+
}
273287
}

crates/secure-memory/src/windows.rs

Lines changed: 71 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ use std::ffi::c_void;
4545
use std::ptr;
4646
use std::sync::OnceLock;
4747

48-
use windows::Win32::System::ErrorReporting::{WerRegisterExcludedMemoryBlock, WerUnregisterExcludedMemoryBlock};
48+
use windows::Win32::System::LibraryLoader::{GetProcAddress, LOAD_LIBRARY_SEARCH_SYSTEM32, LoadLibraryExW};
4949
use windows::Win32::System::Memory::{
5050
MEM_COMMIT, MEM_RELEASE, MEM_RESERVE, PAGE_NOACCESS, PAGE_PROTECTION_FLAGS, PAGE_READONLY, PAGE_READWRITE,
5151
VirtualAlloc, VirtualFree, VirtualLock, VirtualProtect, VirtualUnlock,
@@ -54,6 +54,49 @@ use windows::Win32::System::SystemInformation::{GetSystemInfo, SYSTEM_INFO};
5454

5555
use crate::ProtectionStatus;
5656

57+
/// Function pointer type for `WerRegisterExcludedMemoryBlock`.
58+
type WerRegisterFn = unsafe extern "system" fn(*const c_void, u32) -> windows::core::HRESULT;
59+
60+
/// Function pointer type for `WerUnregisterExcludedMemoryBlock`.
61+
type WerUnregisterFn = unsafe extern "system" fn(*const c_void) -> windows::core::HRESULT;
62+
63+
/// Returns the `WerRegisterExcludedMemoryBlock` function pointer if available on this OS.
64+
///
65+
/// `WerRegisterExcludedMemoryBlock` is not available on all Windows Server 2016 builds
66+
/// (NT 10.0.14393) — it was only backported via later servicing updates.
67+
/// We resolve it dynamically at runtime so the binary loads on any Windows 10/Server 2016
68+
/// build, gracefully degrading to no WER exclusion when the function is absent.
69+
fn wer_register_fn() -> Option<WerRegisterFn> {
70+
static FN: OnceLock<Option<WerRegisterFn>> = OnceLock::new();
71+
*FN.get_or_init(|| {
72+
// SAFETY: `LoadLibraryExW` with `LOAD_LIBRARY_SEARCH_SYSTEM32` is always safe to call.
73+
// The handle is intentionally not freed: `wer.dll` is a system DLL that must
74+
// remain loaded for as long as the cached function pointer is in use.
75+
let hmod = unsafe { LoadLibraryExW(windows::core::w!("wer.dll"), None, LOAD_LIBRARY_SEARCH_SYSTEM32) }.ok()?;
76+
// SAFETY: `hmod` is a valid module handle; the function name is a null-terminated C string.
77+
let proc = unsafe { GetProcAddress(hmod, windows::core::s!("WerRegisterExcludedMemoryBlock")) }?;
78+
// SAFETY: `proc` is the function pointer for `WerRegisterExcludedMemoryBlock`,
79+
// which has signature `fn(*const c_void, u32) -> HRESULT`.
80+
Some(unsafe { std::mem::transmute::<unsafe extern "system" fn() -> isize, WerRegisterFn>(proc) })
81+
})
82+
}
83+
84+
/// Returns the `WerUnregisterExcludedMemoryBlock` function pointer if available on this OS.
85+
fn wer_unregister_fn() -> Option<WerUnregisterFn> {
86+
static FN: OnceLock<Option<WerUnregisterFn>> = OnceLock::new();
87+
*FN.get_or_init(|| {
88+
// SAFETY: `LoadLibraryExW` with `LOAD_LIBRARY_SEARCH_SYSTEM32` is always safe to call.
89+
// The handle is intentionally not freed: `wer.dll` is a system DLL that must
90+
// remain loaded for as long as the cached function pointer is in use.
91+
let hmod = unsafe { LoadLibraryExW(windows::core::w!("wer.dll"), None, LOAD_LIBRARY_SEARCH_SYSTEM32) }.ok()?;
92+
// SAFETY: `hmod` is a valid module handle; the function name is a null-terminated C string.
93+
let proc = unsafe { GetProcAddress(hmod, windows::core::s!("WerUnregisterExcludedMemoryBlock")) }?;
94+
// SAFETY: `proc` is the function pointer for `WerUnregisterExcludedMemoryBlock`,
95+
// which has signature `fn(*const c_void) -> HRESULT`.
96+
Some(unsafe { std::mem::transmute::<unsafe extern "system" fn() -> isize, WerUnregisterFn>(proc) })
97+
})
98+
}
99+
57100
/// Page-based secure allocation for Windows.
58101
pub(crate) struct SecureAlloc<const N: usize> {
59102
/// Start of the entire 3-page `VirtualAlloc` region (the first guard page).
@@ -148,18 +191,30 @@ impl<const N: usize> SecureAlloc<N> {
148191
// Registration covers the full page, not just N bytes, because the
149192
// allocation model is page-based.
150193
//
151-
// SAFETY: `data` is a valid, committed, page-aligned pointer; `ps` is
152-
// exactly one page — the size passed to `VirtualAlloc`.
153-
let wer_hr = unsafe {
154-
WerRegisterExcludedMemoryBlock(data.cast::<c_void>(), u32::try_from(ps).expect("page size fits in u32"))
194+
// `WerRegisterExcludedMemoryBlock` is resolved dynamically: it is absent on
195+
// some Windows Server 2016 (NT 10.0.14393) builds, and loading it statically
196+
// would prevent the binary from starting on those hosts.
197+
let wer_excluded = match wer_register_fn() {
198+
Some(func) => {
199+
// SAFETY: `data` is a valid, committed, page-aligned pointer; `ps` is
200+
// exactly one page — the size passed to `VirtualAlloc`.
201+
let wer_hr = unsafe { func(data.cast::<c_void>(), u32::try_from(ps).expect("page size fits in u32")) };
202+
if wer_hr.is_err() {
203+
tracing::debug!(
204+
"secure-memory: WerRegisterExcludedMemoryBlock failed ({wer_hr:?}); \
205+
the data page will not be excluded from WER crash reports"
206+
);
207+
}
208+
wer_hr.is_ok()
209+
}
210+
None => {
211+
tracing::debug!(
212+
"secure-memory: WerRegisterExcludedMemoryBlock not available on this Windows version; \
213+
the data page will not be excluded from WER crash reports"
214+
);
215+
false
216+
}
155217
};
156-
let wer_excluded = wer_hr.is_ok();
157-
if !wer_excluded {
158-
tracing::debug!(
159-
"secure-memory: WerRegisterExcludedMemoryBlock failed ({wer_hr:?}); \
160-
the data page will not be excluded from WER crash reports"
161-
);
162-
}
163218

164219
// ── Copy secret into the data page ──────────────────────────────────
165220
// SAFETY: `src` (caller stack) and `data` (VirtualAlloc region) are
@@ -229,10 +284,12 @@ impl<const N: usize> Drop for SecureAlloc<N> {
229284

230285
// Unregister WER exclusion before freeing the page.
231286
// Must happen before `VirtualFree` to avoid a dangling registration.
232-
if self.wer_excluded {
287+
if self.wer_excluded
288+
&& let Some(func) = wer_unregister_fn()
289+
{
233290
// SAFETY: `self.data` is the same pointer passed to
234291
// `WerRegisterExcludedMemoryBlock`; still valid here.
235-
let _ = unsafe { WerUnregisterExcludedMemoryBlock(self.data.cast::<c_void>()) };
292+
let _ = unsafe { func(self.data.cast::<c_void>()) };
236293
}
237294

238295
// Release the entire three-page region.

0 commit comments

Comments
 (0)