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}