From 8878f17830934f9af3a3483de68cae59291c8b36 Mon Sep 17 00:00:00 2001 From: JellySquid Date: Wed, 17 Jan 2024 20:48:26 -0600 Subject: [PATCH] Implement version detection for RivaTuner Statistics Server --- .../compatibility/checks/ModuleScanner.java | 107 ++++++++++++++++++ .../client/platform/windows/api/Kernel32.java | 76 ++++++++++++- .../client/platform/windows/api/User32.java | 2 +- .../windows/api/version/LanguageCodePage.java | 16 +++ .../windows/api/version/QueryResult.java | 4 + .../platform/windows/api/version/Version.java | 81 +++++++++++++ .../windows/api/version/VersionInfo.java | 70 ++++++++++++ 7 files changed, 354 insertions(+), 2 deletions(-) create mode 100644 src/main/java/me/jellysquid/mods/sodium/client/platform/windows/api/version/LanguageCodePage.java create mode 100644 src/main/java/me/jellysquid/mods/sodium/client/platform/windows/api/version/QueryResult.java create mode 100644 src/main/java/me/jellysquid/mods/sodium/client/platform/windows/api/version/Version.java create mode 100644 src/main/java/me/jellysquid/mods/sodium/client/platform/windows/api/version/VersionInfo.java diff --git a/src/main/java/me/jellysquid/mods/sodium/client/compatibility/checks/ModuleScanner.java b/src/main/java/me/jellysquid/mods/sodium/client/compatibility/checks/ModuleScanner.java index 0b6a299c72..34ba89fcde 100644 --- a/src/main/java/me/jellysquid/mods/sodium/client/compatibility/checks/ModuleScanner.java +++ b/src/main/java/me/jellysquid/mods/sodium/client/compatibility/checks/ModuleScanner.java @@ -1,10 +1,15 @@ package me.jellysquid.mods.sodium.client.compatibility.checks; +import me.jellysquid.mods.sodium.client.platform.windows.api.Kernel32; +import me.jellysquid.mods.sodium.client.platform.windows.api.version.Version; import net.minecraft.util.WinNativeModuleUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.List; +import java.util.regex.Pattern; /** * Utility class for determining whether the current process has been injected into or otherwise modified. This should @@ -33,11 +38,113 @@ public static void checkModules() { // RivaTuner hooks the wglCreateContext function, and leaves itself behind as a loaded module if (Configuration.WIN32_RTSS_HOOKS && isModuleLoaded(modules, RTSS_HOOKS_MODULE_NAMES)) { + checkRTSSModules(); + } + } + + private static void checkRTSSModules() { + LOGGER.warn("RivaTuner Statistics Server (RTSS) has injected into the process! Attempting to apply workarounds for compatibility..."); + + String version = null; + + try { + version = findRTSSModuleVersion(); + } catch (Throwable t) { + LOGGER.warn("Exception thrown while reading file version", t); + } + + if (version == null) { + LOGGER.warn("Could not determine version of RivaTuner Statistics Server"); + } else { + LOGGER.info("Detected RivaTuner Statistics Server version: {}", version); + } + + if (version == null || !isRTSSCompatible(version)) { throw new RuntimeException("RivaTuner Statistics Server (RTSS) is not compatible with Sodium, " + "see here for more details: https://github.com/CaffeineMC/sodium-fabric/wiki/Known-Issues#rtss-incompatible"); } } + private static final Pattern RTSS_VERSION_PATTERN = Pattern.compile("^(?\\d*), (?\\d*), (?\\d*), (?\\d*)$"); + + private static boolean isRTSSCompatible(String version) { + var matcher = RTSS_VERSION_PATTERN.matcher(version); + + if (!matcher.matches()) { + return false; + } + + try { + int x = Integer.parseInt(matcher.group("x")); + int y = Integer.parseInt(matcher.group("y")); + int z = Integer.parseInt(matcher.group("z")); + + // >=7.3.4 + return x > 7 || (x == 7 && y > 3) || (x == 7 && y == 3 && z >= 4); + } catch (NumberFormatException e) { + LOGGER.warn("Invalid version string: {}", version); + } + + return false; + } + + private static String findRTSSModuleVersion() { + long module; + + try { + module = Kernel32.getModuleHandleByNames(RTSS_HOOKS_MODULE_NAMES); + } catch (Throwable t) { + LOGGER.warn("Failed to locate module", t); + return null; + } + + String moduleFileName; + + try { + moduleFileName = Kernel32.getModuleFileName(module); + } catch (Throwable t) { + LOGGER.warn("Failed to get path of module", t); + return null; + } + + var modulePath = Path.of(moduleFileName); + var moduleDirectory = modulePath.getParent(); + + LOGGER.info("Searching directory: {}", moduleDirectory); + + var executablePath = moduleDirectory.resolve("RTSS.exe"); + + if (!Files.exists(executablePath)) { + LOGGER.warn("Could not find executable: {}", executablePath); + return null; + } + + LOGGER.info("Parsing file: {}", executablePath); + + var version = Version.getModuleFileVersion(executablePath.toAbsolutePath().toString()); + + if (version == null) { + LOGGER.warn("Couldn't find version structure"); + return null; + } + + var translation = version.queryEnglishTranslation(); + + if (translation == null) { + LOGGER.warn("Couldn't find suitable translation"); + return null; + } + + var fileVersion = version.queryValue("FileVersion", translation); + + if (fileVersion == null) { + LOGGER.warn("Couldn't query file version"); + return null; + } + + return fileVersion; + } + private static boolean isModuleLoaded(List modules, String[] names) { for (var name : names) { for (var module : modules) { diff --git a/src/main/java/me/jellysquid/mods/sodium/client/platform/windows/api/Kernel32.java b/src/main/java/me/jellysquid/mods/sodium/client/platform/windows/api/Kernel32.java index 7781671b99..d85c4852b8 100644 --- a/src/main/java/me/jellysquid/mods/sodium/client/platform/windows/api/Kernel32.java +++ b/src/main/java/me/jellysquid/mods/sodium/client/platform/windows/api/Kernel32.java @@ -1,19 +1,35 @@ package me.jellysquid.mods.sodium.client.platform.windows.api; import org.jetbrains.annotations.Nullable; +import org.lwjgl.PointerBuffer; import org.lwjgl.system.*; import java.nio.ByteBuffer; +import java.nio.IntBuffer; public class Kernel32 { - private static final SharedLibrary LIBRARY = Library.loadNative("me.jellyquid.mods.sodium", "kernel32"); + private static final SharedLibrary LIBRARY = APIUtil.apiCreateLibrary("kernel32"); + + private static final int MAX_PATH = 32767; + + private static final int GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT = 1 << 0; + private static final int GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS = 1 << 2; private static final long PFN_GetCommandLineW; private static final long PFN_SetEnvironmentVariableW; + private static final long PFN_GetModuleHandleExW; + private static final long PFN_GetLastError; + + private static final long PFN_GetModuleFileNameW; + + static { PFN_GetCommandLineW = APIUtil.apiGetFunctionAddress(LIBRARY, "GetCommandLineW"); PFN_SetEnvironmentVariableW = APIUtil.apiGetFunctionAddress(LIBRARY, "SetEnvironmentVariableW"); + PFN_GetModuleHandleExW = APIUtil.apiGetFunctionAddress(LIBRARY, "GetModuleHandleExW"); + PFN_GetLastError = APIUtil.apiGetFunctionAddress(LIBRARY, "GetLastError"); + PFN_GetModuleFileNameW = APIUtil.apiGetFunctionAddress(LIBRARY, "GetModuleFileNameW"); } public static void setEnvironmentVariable(String name, @Nullable String value) { @@ -35,4 +51,62 @@ public static void setEnvironmentVariable(String name, @Nullable String value) { public static long getCommandLine() { return JNI.callP(PFN_GetCommandLineW); } + + public static long getModuleHandleByNames(String[] names) { + for (String name : names) { + var handle = getModuleHandleByName(name); + + if (handle != MemoryUtil.NULL) { + return handle; + } + } + + throw new RuntimeException("Could not obtain handle of module"); + } + + public static long getModuleHandleByName(String name) { + try (MemoryStack stack = MemoryStack.stackPush()) { + ByteBuffer lpFunctionNameBuf = stack.malloc(16, MemoryUtil.memLengthUTF16(name, true)); + MemoryUtil.memUTF16(name, true, lpFunctionNameBuf); + + PointerBuffer phModule = stack.callocPointer(1); + + int result; + result = JNI.callPPI(GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + MemoryUtil.memAddress(lpFunctionNameBuf), MemoryUtil.memAddress(phModule), PFN_GetModuleHandleExW); + + if (result == 0) { + var error = getLastError(); + + switch (error) { + case 126 /* ERROR_MOD_NOT_FOUND */: + return MemoryUtil.NULL; + default: + throw new RuntimeException("GetModuleHandleEx failed, error=" + error); + } + } + + return phModule.get(0); + } + } + + public static String getModuleFileName(long phModule) { + ByteBuffer lpFileName = MemoryUtil.memAlignedAlloc(16, MAX_PATH); + + try { + int length = JNI.callPPI(phModule, MemoryUtil.memAddress(lpFileName), lpFileName.capacity(), PFN_GetModuleFileNameW); + + if (length == 0) { + throw new RuntimeException("GetModuleFileNameW failed, error=" + getLastError()); + } + + return MemoryUtil.memUTF16(lpFileName, length); + } finally { + MemoryUtil.memAlignedFree(lpFileName); + } + } + + public static int getLastError() { + return JNI.callI(PFN_GetLastError); + } } diff --git a/src/main/java/me/jellysquid/mods/sodium/client/platform/windows/api/User32.java b/src/main/java/me/jellysquid/mods/sodium/client/platform/windows/api/User32.java index d74bcee441..d4892a5249 100644 --- a/src/main/java/me/jellysquid/mods/sodium/client/platform/windows/api/User32.java +++ b/src/main/java/me/jellysquid/mods/sodium/client/platform/windows/api/User32.java @@ -7,7 +7,7 @@ import org.lwjgl.system.SharedLibrary; public class User32 { - private static final SharedLibrary LIBRARY = Library.loadNative("me.jellyquid.mods.sodium", "user32"); + private static final SharedLibrary LIBRARY = APIUtil.apiCreateLibrary("user32"); private static final long PFN_MessageBoxIndirectW; diff --git a/src/main/java/me/jellysquid/mods/sodium/client/platform/windows/api/version/LanguageCodePage.java b/src/main/java/me/jellysquid/mods/sodium/client/platform/windows/api/version/LanguageCodePage.java new file mode 100644 index 0000000000..27fa38e395 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/platform/windows/api/version/LanguageCodePage.java @@ -0,0 +1,16 @@ +package me.jellysquid.mods.sodium.client.platform.windows.api.version; + +import org.lwjgl.system.MemoryUtil; + +public record LanguageCodePage(int languageId, int codePage) { + static final int STRIDE = Integer.BYTES; + + static LanguageCodePage decode(long address) { + var value = MemoryUtil.memGetInt(address); + + int languageId = value & 0xFFFF; + int codePage = (value & 0xFFFF0000) >> 16; + + return new LanguageCodePage(languageId, codePage); + } +} \ No newline at end of file diff --git a/src/main/java/me/jellysquid/mods/sodium/client/platform/windows/api/version/QueryResult.java b/src/main/java/me/jellysquid/mods/sodium/client/platform/windows/api/version/QueryResult.java new file mode 100644 index 0000000000..ffe9ad84f9 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/platform/windows/api/version/QueryResult.java @@ -0,0 +1,4 @@ +package me.jellysquid.mods.sodium.client.platform.windows.api.version; + +public record QueryResult(long address, int length) { +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/platform/windows/api/version/Version.java b/src/main/java/me/jellysquid/mods/sodium/client/platform/windows/api/version/Version.java new file mode 100644 index 0000000000..1151edd12c --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/platform/windows/api/version/Version.java @@ -0,0 +1,81 @@ +package me.jellysquid.mods.sodium.client.platform.windows.api.version; + +import me.jellysquid.mods.sodium.client.platform.windows.api.Kernel32; +import org.jetbrains.annotations.Nullable; +import org.lwjgl.PointerBuffer; +import org.lwjgl.system.*; + +import java.nio.ByteBuffer; +import java.nio.IntBuffer; + +public class Version { + private static final SharedLibrary LIBRARY = APIUtil.apiCreateLibrary("version"); + + private static final long PFN_GetFileVersionInfoSizeW; + private static final long PFN_GetFileVersionInfoW; + + private static final long PFN_VerQueryValueW; + + static { + PFN_GetFileVersionInfoSizeW = APIUtil.apiGetFunctionAddress(LIBRARY, "GetFileVersionInfoSizeW"); + PFN_GetFileVersionInfoW = APIUtil.apiGetFunctionAddress(LIBRARY, "GetFileVersionInfoW"); + + PFN_VerQueryValueW = APIUtil.apiGetFunctionAddress(LIBRARY, "VerQueryValueW"); + } + + + static @Nullable QueryResult query(ByteBuffer pBlock, String subBlock) { + try (MemoryStack stack = MemoryStack.stackPush()) { + ByteBuffer pSubBlock = stack.malloc(16, MemoryUtil.memLengthUTF16(subBlock, true)); + MemoryUtil.memUTF16(subBlock, true, pSubBlock); + + PointerBuffer pBuffer = stack.callocPointer(1); + IntBuffer pLen = stack.callocInt(1); + + int result = JNI.callPPPPI(MemoryUtil.memAddress(pBlock), MemoryUtil.memAddress(pSubBlock), + MemoryUtil.memAddress(pBuffer), MemoryUtil.memAddress(pLen), Version.PFN_VerQueryValueW); + + if (result == 0) { + return null; + } + + return new QueryResult(pBuffer.get(), pLen.get()); + } + } + + public static @Nullable VersionInfo getModuleFileVersion(String filename) { + ByteBuffer lptstrFilename = MemoryUtil.memAlignedAlloc(16, MemoryUtil.memLengthUTF16(filename, true)); + + try (MemoryStack stack = MemoryStack.stackPush()) { + MemoryUtil.memUTF16(filename, true, lptstrFilename); + + IntBuffer lpdwHandle = stack.callocInt(1); + int versionInfoLength = JNI.callPPI(MemoryUtil.memAddress(lptstrFilename), MemoryUtil.memAddress(lpdwHandle), PFN_GetFileVersionInfoSizeW); + + if (versionInfoLength == 0) { + int error = Kernel32.getLastError(); + + switch (error) { + case 0x714 /* ERROR_RESOURCE_DATA_NOT_FOUND */: + case 0x715 /* ERROR_RESOURCE_TYPE_NOT_FOUND */: + return null; + default: + throw new RuntimeException("GetFileVersionInfoSizeW failed, error=" + error); + } + } + + VersionInfo versionInfo = VersionInfo.allocate(versionInfoLength); + int result = JNI.callPPI(MemoryUtil.memAddress(lptstrFilename), lpdwHandle.get(), versionInfoLength, versionInfo.address(), PFN_GetFileVersionInfoW); + + if (result == 0) { + versionInfo.close(); + + throw new RuntimeException("GetFileVersionInfoW failed, error=" + Kernel32.getLastError()); + } + + return versionInfo; + } finally { + MemoryUtil.memAlignedFree(lptstrFilename); + } + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/platform/windows/api/version/VersionInfo.java b/src/main/java/me/jellysquid/mods/sodium/client/platform/windows/api/version/VersionInfo.java new file mode 100644 index 0000000000..ea277b508d --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/platform/windows/api/version/VersionInfo.java @@ -0,0 +1,70 @@ +package me.jellysquid.mods.sodium.client.platform.windows.api.version; + +import org.jetbrains.annotations.Nullable; +import org.lwjgl.system.MemoryUtil; + +import java.io.Closeable; +import java.nio.ByteBuffer; +import java.util.Locale; + +public class VersionInfo implements Closeable { + private final ByteBuffer pBlock; + + VersionInfo(ByteBuffer buffer) { + this.pBlock = buffer; + } + + public static VersionInfo allocate(int len) { + return new VersionInfo(MemoryUtil.memAlignedAlloc(16, len)); + } + + public @Nullable String queryValue(String key, LanguageCodePage translation) { + var result = Version.query(this.pBlock, getStringFileInfoPath(key, translation)); + + if (result == null) { + return null; + } + + return MemoryUtil.memUTF16(result.address()); + } + + public @Nullable LanguageCodePage queryEnglishTranslation() { + var result = Version.query(this.pBlock, "\\VarFileInfo\\Translation"); + + if (result == null) { + return null; + } + + return findEnglishTranslationEntry(result); + } + + private static @Nullable LanguageCodePage findEnglishTranslationEntry(final QueryResult result) { + LanguageCodePage translation = null; + + int offset = 0; + + while (offset < result.length()) { + translation = LanguageCodePage.decode(result.address() + offset); + offset += LanguageCodePage.STRIDE; + + if (translation.codePage() == 1200 /* UTF-16LE */ && translation.languageId() == 1033 /* English, United States */) { + return translation; + } + } + + return translation; + } + + private static String getStringFileInfoPath(String key, LanguageCodePage translation) { + return String.format(Locale.ROOT, "\\StringFileInfo\\%04x%04x\\%s", translation.languageId(), translation.codePage(), key); + } + + @Override + public void close() { + MemoryUtil.memAlignedFree(this.pBlock); + } + + long address() { + return MemoryUtil.memAddress(this.pBlock); + } +}