//! Transparent forwarding for the system `version.dll` API. //! //! The hook is deployed under the `version.dll` filename, so every VERSION API //! import must continue to behave exactly as it would without OpenFUT. Resolve //! the genuine system DLL once during process attach, then tail-jump from each //! exported stub. A tail jump preserves the caller's complete Windows x64 ABI //! state, including stack arguments whose signatures differ between exports. use std::sync::atomic::{AtomicUsize, Ordering}; use windows_sys::Win32::System::LibraryLoader::{GetProcAddress, LoadLibraryW}; const EXPORT_COUNT: usize = 16; /// Keep this list in the same order as the generated stubs below. const EXPORTS: [&[u8]; EXPORT_COUNT] = [ b"GetFileVersionInfoA\0", b"GetFileVersionInfoExA\0", b"GetFileVersionInfoExW\0", b"GetFileVersionInfoSizeA\0", b"GetFileVersionInfoSizeExA\0", b"GetFileVersionInfoSizeExW\0", b"GetFileVersionInfoSizeW\0", b"GetFileVersionInfoW\0", b"VerFindFileA\0", b"VerFindFileW\0", b"VerInstallFileA\0", b"VerInstallFileW\0", b"VerLanguageNameA\0", b"VerLanguageNameW\0", b"VerQueryValueA\0", b"VerQueryValueW\0", ]; /// Addresses in the genuine system DLL. Atomic storage gives the assembly /// stubs stable, directly addressable pointer-sized slots without `static mut`. static REAL: [AtomicUsize; EXPORT_COUNT] = [const { AtomicUsize::new(0) }; EXPORT_COUNT]; macro_rules! proxy_stub { ($index:literal, $name:ident) => { #[unsafe(no_mangle)] #[unsafe(naked)] pub unsafe extern "system" fn $name() { core::arch::naked_asm!( "jmp qword ptr [rip + {base} + {offset}]", base = sym REAL, offset = const $index * size_of::(), ); } }; } proxy_stub!(0, GetFileVersionInfoA); proxy_stub!(1, GetFileVersionInfoExA); proxy_stub!(2, GetFileVersionInfoExW); proxy_stub!(3, GetFileVersionInfoSizeA); proxy_stub!(4, GetFileVersionInfoSizeExA); proxy_stub!(5, GetFileVersionInfoSizeExW); proxy_stub!(6, GetFileVersionInfoSizeW); proxy_stub!(7, GetFileVersionInfoW); proxy_stub!(8, VerFindFileA); proxy_stub!(9, VerFindFileW); proxy_stub!(10, VerInstallFileA); proxy_stub!(11, VerInstallFileW); proxy_stub!(12, VerLanguageNameA); proxy_stub!(13, VerLanguageNameW); proxy_stub!(14, VerQueryValueA); proxy_stub!(15, VerQueryValueW); /// Resolve forwarding targets before returning from `DLL_PROCESS_ATTACH`. /// Calls into our exports may happen as soon as the loader releases its lock, /// so deferring this operation to the hook worker would create a race. pub(crate) unsafe fn resolve() -> bool { // Loading by absolute path prevents this proxy from recursively loading // itself. Proton/Wine exposes the Windows system directory at this path. let path: Vec = "C:\\Windows\\System32\\version.dll\0" .encode_utf16() .collect(); let module = LoadLibraryW(path.as_ptr()); if module.is_null() { crate::write_log("version_proxy: FATAL: system version.dll load failed\n"); return false; } let mut missing = 0; for (slot, name) in REAL.iter().zip(EXPORTS) { let address = GetProcAddress(module, name.as_ptr()).map_or(0, |proc| proc as usize); slot.store(address, Ordering::Release); if address == 0 { missing += 1; } } if missing == 0 { crate::write_log("version_proxy: forwarded all 16 exports\n"); true } else { crate::write_log(&format!( "version_proxy: FATAL: {missing}/16 system exports missing\n" )); false } } #[cfg(test)] mod tests { use super::*; #[test] fn export_table_is_complete_and_nul_terminated() { assert_eq!(EXPORTS.len(), EXPORT_COUNT); assert!(EXPORTS.iter().all(|name| name.last() == Some(&0))); assert!(EXPORTS .iter() .all(|name| !name[..name.len() - 1].contains(&0))); } }