master ohmu/fastfetch-openbsd / src / detection / wm / wm_windows.c
ohmu  ·  2026-10-15
  1#include "wm.h"
  2#include "common/debug.h"
  3#include "common/mallocHelper.h"
  4#include "common/io.h"
  5#include "common/library.h"
  6#include "common/processing.h"
  7#include "common/windows/folders.h"
  8#include "common/windows/nt.h"
  9#include "common/windows/unicode.h"
 10#include "common/windows/version.h"
 11
 12#include <stdalign.h>
 13#include <windows.h>
 14#include <ntstatus.h>
 15#include <softpub.h>
 16
 17typedef enum {
 18    FF_PROCESS_TYPE_NONE,
 19    FF_PROCESS_TYPE_SIGNED = 1 << 0,
 20    FF_PROCESS_TYPE_WINDOWS_STORE = 1 << 1,
 21    FF_PROCESS_TYPE_GUI = 1 << 2,
 22    FF_PROCESS_TYPE_CUI = 1 << 3,
 23} FFProcessType;
 24
 25static bool verifySignature(const wchar_t* filePath) {
 26    FF_LIBRARY_LOAD(wintrust, true, "wintrust" FF_LIBRARY_EXTENSION, -1)
 27    FF_LIBRARY_LOAD_SYMBOL(wintrust, WinVerifyTrustEx, true)
 28
 29    WINTRUST_FILE_INFO fileInfo = {
 30        .cbStruct = sizeof(fileInfo),
 31        .pcwszFilePath = filePath,
 32    };
 33
 34    GUID actionID = WINTRUST_ACTION_GENERIC_VERIFY_V2;
 35
 36    WINTRUST_DATA trustData = {
 37        .cbStruct = sizeof(trustData),
 38        .dwUIChoice = WTD_UI_NONE,
 39        .fdwRevocationChecks = WTD_REVOKE_NONE,
 40        .dwUnionChoice = WTD_CHOICE_FILE,
 41        .pFile = &fileInfo,
 42        .dwStateAction = WTD_STATEACTION_VERIFY,
 43        .dwProvFlags = WTD_SAFER_FLAG,
 44    };
 45
 46    LONG status = ffWinVerifyTrustEx(nullptr, &actionID, &trustData);
 47    trustData.dwStateAction = WTD_STATEACTION_CLOSE;
 48    ffWinVerifyTrustEx(nullptr, &actionID, &trustData);
 49
 50    return status == ERROR_SUCCESS;
 51}
 52
 53static bool isProcessTrusted(DWORD processId, FFProcessType processType, UNICODE_STRING* buffer, size_t bufSize) {
 54    FF_AUTO_CLOSE_FD HANDLE hProcess = nullptr;
 55    if (!NT_SUCCESS(NtOpenProcess(&hProcess, PROCESS_QUERY_LIMITED_INFORMATION, &(OBJECT_ATTRIBUTES) {
 56                                                                                    .Length = sizeof(OBJECT_ATTRIBUTES),
 57                                                                                },
 58            &(CLIENT_ID) { .UniqueProcess = (HANDLE) (uintptr_t) processId }))) {
 59        return false;
 60    }
 61
 62    ULONG size;
 63    if (!NT_SUCCESS(NtQueryInformationProcess(hProcess, ProcessImageFileNameWin32, buffer, (ULONG) bufSize, &size)) ||
 64        buffer->Length == 0) {
 65        return false;
 66    }
 67    assert(buffer->MaximumLength >= buffer->Length + 2); // nullptr terminated
 68
 69    if (processType & FF_PROCESS_TYPE_WINDOWS_STORE) {
 70        static wchar_t windowsAppsPath[MAX_PATH];
 71        static uint32_t windowsAppsPathLen;
 72        if (windowsAppsPathLen == 0) {
 73            FF_STRBUF_AUTO_DESTROY programFiles = ffStrbufCreate();
 74            ULONG length = 0; // in bytes, including the null terminator
 75            if (ffGetKnownFolderPath(FF_KNOWN_FOLDER_PROGRAM_FILES, &programFiles)) {
 76                ffStrbufAppendS(&programFiles, "\\WindowsApps\\");
 77                if (NT_SUCCESS(RtlUTF8ToUnicodeN(windowsAppsPath, (ULONG) sizeof(windowsAppsPath), &length, programFiles.chars, (ULONG) programFiles.length + 1))) {
 78                    windowsAppsPathLen = (uint32_t) (length / sizeof(wchar_t) - 1);
 79                } else {
 80                    windowsAppsPathLen = -1u;
 81                }
 82            } else {
 83                windowsAppsPathLen = -1u;
 84            }
 85        }
 86        if (windowsAppsPathLen != -1u &&
 87            (buffer->Length <= windowsAppsPathLen * sizeof(wchar_t) ||               // Path is too short to be in WindowsApps
 88                _wcsnicmp(buffer->Buffer, windowsAppsPath, windowsAppsPathLen) != 0) // Path does not start with WindowsApps
 89        ) {
 90            return false;
 91        }
 92    }
 93
 94    if (processType & FF_PROCESS_TYPE_SIGNED) {
 95        if (!verifySignature(buffer->Buffer)) {
 96            return false;
 97        }
 98    }
 99
100    if (processType & (FF_PROCESS_TYPE_GUI | FF_PROCESS_TYPE_CUI)) {
101        SECTION_IMAGE_INFORMATION info = {};
102        if (!NT_SUCCESS(NtQueryInformationProcess(hProcess, ProcessImageInformation, &info, sizeof(info), &size)) ||
103            size != sizeof(info)) {
104            return false;
105        }
106
107        if ((processType & FF_PROCESS_TYPE_GUI) && info.SubSystemType != IMAGE_SUBSYSTEM_WINDOWS_GUI) {
108            return false;
109        }
110        if ((processType & FF_PROCESS_TYPE_CUI) && info.SubSystemType != IMAGE_SUBSYSTEM_WINDOWS_CUI) {
111            return false;
112        }
113    }
114
115    return true;
116}
117
118#define ffStrEqualNWS(str, compareTo) (_wcsnicmp(str, L##compareTo, sizeof(compareTo) - 1) == 0)
119
120// Returns true if the process was recognized as a WM plugin and pluginName was set.
121static bool handleProcess(FFstrbuf* pluginName, uint32_t pid, const UNICODE_STRING* imageName) {
122    alignas(UNICODE_STRING) uint8_t buffer[4096];
123    UNICODE_STRING* filePath = (UNICODE_STRING*) buffer;
124
125    if (imageName->Length == strlen("FancyWM-GUI.exe") * sizeof(wchar_t) &&
126        ffStrEqualNWS(imageName->Buffer, "FancyWM-GUI.exe") &&
127        isProcessTrusted(pid, FF_PROCESS_TYPE_WINDOWS_STORE | FF_PROCESS_TYPE_GUI, filePath, sizeof(buffer))) {
128        if (instance.config.general.detectVersion && ffGetFileVersion(filePath->Buffer, nullptr, pluginName)) {
129            ffStrbufPrependS(pluginName, "FancyWM ");
130        } else {
131            ffStrbufSetStatic(pluginName, "FancyWM");
132        }
133        return true;
134    } else if (imageName->Length == strlen("glazewm-watcher.exe") * sizeof(wchar_t) &&
135        ffStrEqualNWS(imageName->Buffer, "glazewm-watcher.exe") &&
136        isProcessTrusted(pid, FF_PROCESS_TYPE_SIGNED | FF_PROCESS_TYPE_GUI, filePath, sizeof(buffer))) {
137        if (instance.config.general.detectVersion && ffGetFileVersion(filePath->Buffer, nullptr, pluginName)) {
138            ffStrbufPrependS(pluginName, "GlazeWM ");
139        } else {
140            ffStrbufSetStatic(pluginName, "GlazeWM");
141        }
142        return true;
143    } else if (imageName->Length == strlen("komorebi.exe") * sizeof(wchar_t) &&
144        ffStrEqualNWS(imageName->Buffer, "komorebi.exe") &&
145        isProcessTrusted(pid, FF_PROCESS_TYPE_CUI, filePath, sizeof(buffer))) {
146        if (instance.config.general.detectVersion) {
147            FF_STRBUF_AUTO_DESTROY path = ffStrbufCreateNWS(filePath->Length / sizeof(wchar_t), filePath->Buffer);
148            if (ffProcessAppendStdOut(pluginName, (char* const[]) {
149                                                      path.chars,
150                                                      "--version",
151                                                      nullptr,
152                                                  }) == nullptr) {
153                ffStrbufSubstrBeforeFirstC(pluginName, '\n');
154            }
155        }
156        if (pluginName->length == 0) {
157            ffStrbufSetStatic(pluginName, "Komorebi");
158        }
159        return true;
160    }
161
162    return false;
163}
164
165const char* ffDetectWMPlugin(FFstrbuf* pluginName) {
166    if (ffIsSystemBasicProcessInfoAvailable()) {
167        // Unlike SystemProcessInformation, SystemBasicProcessInformation only returns processes
168        // (no threads), so a single query with a modest buffer is enough.
169        ULONG size = 0;
170        if (NtQuerySystemInformation(SystemBasicProcessInformation, nullptr, 0, &size) != STATUS_INFO_LENGTH_MISMATCH) {
171            goto fallback;
172        }
173        // The process table may change between the two calls; retry with a larger buffer.
174        size += size / 8 + sizeof(SYSTEM_BASICPROCESS_INFORMATION);
175        FF_AUTO_FREE SYSTEM_BASICPROCESS_INFORMATION* pstart = malloc(size);
176
177        if (!NT_SUCCESS(NtQuerySystemInformation(SystemBasicProcessInformation, pstart, size, &size))) {
178            goto fallback;
179        }
180
181        for (auto ptr = pstart;; ptr = (SYSTEM_BASICPROCESS_INFORMATION*) ((uint8_t*) ptr + ptr->NextEntryOffset)) {
182            assert(ptr >= pstart && (uint8_t*) ptr < (uint8_t*) pstart + size);
183            if (handleProcess(pluginName, (uint32_t)(uintptr_t) ptr->UniqueProcessId, &ptr->ImageName)) {
184                return nullptr;
185            }
186            // The last process in the list always has a NextEntryOffset of 0, even if the buffer was truncated.
187            if (!ptr->NextEntryOffset) {
188                break;
189            }
190        }
191        return nullptr;
192    }
193
194fallback:
195    {
196        FF_AUTO_FREE SYSTEM_PROCESS_INFORMATION* pstart = nullptr;
197
198        // Multiple attempts in case processes change while
199        // we are in the middle of querying them.
200        ULONG size = 0;
201        for (int attempts = 0;; ++attempts) {
202            if (size) {
203                pstart = (SYSTEM_PROCESS_INFORMATION*) realloc(pstart, size);
204                assert(pstart);
205            }
206            NTSTATUS status = NtQuerySystemInformation(SystemProcessInformation, pstart, size, &size);
207            if (NT_SUCCESS(status)) {
208                break;
209            } else if (status == STATUS_INFO_LENGTH_MISMATCH && attempts < 4) {
210                size += sizeof(SYSTEM_PROCESS_INFORMATION) * 5;
211            } else {
212                FF_DEBUG("NtQuerySystemInformation(SystemProcessInformation) failed: %s", ffDebugNtStatus(status));
213                return "NtQuerySystemInformation(SystemProcessInformation) failed";
214            }
215        }
216
217        for (auto ptr = pstart;; ptr = (SYSTEM_PROCESS_INFORMATION*) ((uint8_t*) ptr + ptr->NextEntryOffset)) {
218            assert(ptr->ImageName.Length == 0 || ptr->ImageName.MaximumLength >= ptr->ImageName.Length + 2); // nullptr terminated
219            if (handleProcess(pluginName, (uint32_t)(uintptr_t) ptr->UniqueProcessId, &ptr->ImageName)) {
220                return nullptr;
221            }
222            if (ptr->NextEntryOffset == 0) {
223                break;
224            }
225        }
226    }
227
228    return nullptr;
229}
230
231const char* ffDetectWMVersion(const FFstrbuf* wmName, FFstrbuf* result, [[maybe_unused]] FFWMOptions* options) {
232    if (!wmName) {
233        FF_DEBUG("No WM detected");
234        return "No WM detected";
235    }
236
237    if (ffStrbufEqualS(wmName, "dwm.exe")) {
238        wchar_t exePath[MAX_PATH];
239        _snwprintf(exePath, ARRAY_SIZE(exePath), L"%ls\\system32\\%ls", (const wchar_t*) SharedUserData->NtSystemRoot, L"dwm.exe");
240        ffGetFileVersion(exePath, nullptr, result);
241
242        return nullptr;
243    }
244    FF_DEBUG("Not supported on this platform");
245    return "Not supported on this platform";
246}