ohmu
·
2026-10-15
1#include "registry.h"
2#include "unicode.h"
3#include "common/mallocHelper.h"
4#include "common/debug.h"
5#include "common/windows/nt.h"
6
7#include <stdalign.h>
8#include <ntstatus.h>
9
10static HANDLE hRootKeys[8 /*(uintptr_t) HKEY_CURRENT_USER_LOCAL_SETTINGS - (uintptr_t) HKEY_CLASSES_ROOT + 1*/];
11
12static const char* hKey2Str(HANDLE hRootKey) {
13#define HKEY_CASE(compareKey) \
14 if (hRootKey == hRootKeys[(uintptr_t) compareKey - (uintptr_t) HKEY_CLASSES_ROOT]) return #compareKey;
15 HKEY_CASE(HKEY_CLASSES_ROOT)
16 HKEY_CASE(HKEY_CURRENT_USER)
17 HKEY_CASE(HKEY_LOCAL_MACHINE)
18 HKEY_CASE(HKEY_USERS)
19 HKEY_CASE(HKEY_PERFORMANCE_DATA)
20 HKEY_CASE(HKEY_CURRENT_CONFIG)
21 HKEY_CASE(HKEY_DYN_DATA)
22 HKEY_CASE(HKEY_CURRENT_USER_LOCAL_SETTINGS)
23#undef HKEY_CASE
24
25 return "UNKNOWN";
26}
27
28HANDLE ffRegGetRootKeyHandle(HKEY hKey) {
29 assert(hKey);
30 assert((uintptr_t) hKey >= (uintptr_t) HKEY_CLASSES_ROOT && (uintptr_t) hKey <= (uintptr_t) HKEY_CURRENT_USER_LOCAL_SETTINGS);
31
32 FF_DEBUG("Getting root key handle for HKEY %08llx", (uint64_t) (uintptr_t) hKey);
33
34 HANDLE result = hRootKeys[(uintptr_t) hKey - (uintptr_t) HKEY_CLASSES_ROOT];
35 if (result) {
36 FF_DEBUG("Found cached root key handle for %s -> %p", hKey2Str(result), result);
37 return result;
38 }
39
40 switch ((uintptr_t) hKey) {
41 case (uintptr_t) HKEY_CURRENT_USER: {
42 NTSTATUS status = RtlOpenCurrentUser(KEY_READ, &result);
43 if (!NT_SUCCESS(status)) {
44 FF_DEBUG("RtlOpenCurrentUser() failed: %s", ffDebugNtStatus(status));
45 return nullptr;
46 }
47 break;
48 }
49
50 case (uintptr_t) HKEY_LOCAL_MACHINE: {
51 UNICODE_STRING path = RTL_CONSTANT_STRING(L"\\Registry\\Machine");
52 NTSTATUS status = NtOpenKey(&result, KEY_READ, &(OBJECT_ATTRIBUTES) {
53 .Length = sizeof(OBJECT_ATTRIBUTES),
54 .RootDirectory = nullptr,
55 .ObjectName = &path,
56 .Attributes = OBJ_CASE_INSENSITIVE,
57 });
58 if (!NT_SUCCESS(status)) {
59 FF_DEBUG("NtOpenKey(%ls) failed: %s (0x%08lx)", path.Buffer, ffDebugNtStatus(status), status);
60 return nullptr;
61 }
62 break;
63 }
64 default:
65 // Unsupported
66 FF_DEBUG("Unsupported root key: %p", hKey);
67 assert(false);
68 return nullptr;
69 }
70 hRootKeys[(uintptr_t) hKey - (uintptr_t) HKEY_CLASSES_ROOT] = result;
71 FF_DEBUG("Opened root key %s -> %p", hKey2Str(result), result);
72 return result;
73}
74
75bool ffRegOpenSubkeyForRead(HANDLE hKey, const wchar_t* subKeyW, HANDLE* result, FFstrbuf* error) {
76 assert(hKey);
77 assert(subKeyW);
78 assert(result);
79
80 FF_DEBUG("Opening subkey %s\\%ls for read", hKey2Str(hKey), subKeyW);
81
82 USHORT subKeyLen = (USHORT) (wcslen(subKeyW) * sizeof(wchar_t));
83 if (!NT_SUCCESS(NtOpenKey(result, KEY_READ, &(OBJECT_ATTRIBUTES) {
84 .Length = sizeof(OBJECT_ATTRIBUTES),
85 .RootDirectory = hKey,
86 .ObjectName = &(UNICODE_STRING) {
87 .Length = subKeyLen,
88 .MaximumLength = subKeyLen + (USHORT) sizeof(wchar_t),
89 .Buffer = (wchar_t*) subKeyW,
90 },
91 }))) {
92 FF_DEBUG("NtOpenKey(%s\\<subkey>) failed", hKey2Str(hKey));
93 if (error) {
94 FF_STRBUF_AUTO_DESTROY subKeyA = ffStrbufCreateWS(subKeyW);
95 ffStrbufAppendF(error, "NtOpenKey(%s\\%s) failed", hKey2Str(hKey), subKeyA.chars);
96 }
97 return false;
98 }
99 FF_DEBUG("Opened subkey under %s -> %p", hKey2Str(hKey), *result);
100 return true;
101}
102
103static bool processRegValue(const FFRegValueArg* arg, const ULONG regType, const void* regData, ULONG regDataLen, FFstrbuf* error) {
104 switch (arg->type) {
105 case FF_ARG_TYPE_STRBUF: {
106 if (regType != REG_SZ && regType != REG_EXPAND_SZ) {
107 goto type_mismatch;
108 }
109
110 FFstrbuf* strbuf = (FFstrbuf*) arg->value;
111 uint32_t strLen = regDataLen / sizeof(wchar_t);
112 if (strLen == 0) {
113 ffStrbufClear(strbuf);
114 } else {
115 const wchar_t* ws = (const wchar_t*) regData;
116 if (ws[strLen - 1] == L'\0') {
117 --strLen;
118 }
119 ffStrbufSetNWS(strbuf, strLen, ws);
120 }
121 break;
122 }
123
124 case FF_ARG_TYPE_UINT:
125 case FF_ARG_TYPE_UINT64:
126 case FF_ARG_TYPE_UINT16:
127 case FF_ARG_TYPE_UINT8:
128 case FF_ARG_TYPE_BOOL: {
129 uint64_t value = 0;
130
131 if (regType == REG_DWORD) {
132 if (regDataLen < sizeof(uint32_t)) {
133 goto type_mismatch;
134 }
135 value = *(uint32_t*) regData;
136 } else if (regType == REG_QWORD) {
137 if (regDataLen < sizeof(uint64_t)) {
138 goto type_mismatch;
139 }
140 value = *(uint64_t*) regData;
141 } else {
142 goto type_mismatch;
143 }
144
145 if (arg->type == FF_ARG_TYPE_UINT) {
146 *(uint32_t*) arg->value = (uint32_t) value;
147 } else if (arg->type == FF_ARG_TYPE_UINT64) {
148 *(uint64_t*) arg->value = (uint64_t) value;
149 } else if (arg->type == FF_ARG_TYPE_UINT16) {
150 *(uint16_t*) arg->value = (uint16_t) value;
151 } else if (arg->type == FF_ARG_TYPE_UINT8) {
152 *(uint8_t*) arg->value = (uint8_t) value;
153 } else if (arg->type == FF_ARG_TYPE_BOOL) {
154 *(bool*) arg->value = value != 0;
155 }
156 break;
157 }
158
159 case FF_ARG_TYPE_FLOAT: {
160 if (regDataLen < sizeof(float)) {
161 goto type_mismatch;
162 }
163 *(float*) arg->value = *(float*) regData;
164 break;
165 }
166
167 case FF_ARG_TYPE_DOUBLE: {
168 if (regDataLen < sizeof(double)) {
169 goto type_mismatch;
170 }
171 *(double*) arg->value = *(double*) regData;
172 break;
173 }
174
175 case FF_ARG_TYPE_LIST: {
176 if (regType != REG_MULTI_SZ) {
177 goto type_mismatch;
178 }
179
180 FFlist* list = (FFlist*) arg->value;
181 ffListClear(list);
182
183 for (
184 const wchar_t* ptr = (const wchar_t*) regData;
185 (const uint8_t*) ptr < (const uint8_t*) regData + regDataLen && *ptr;
186 ptr++) {
187 uint32_t strLen = (uint32_t) wcsnlen(ptr, regDataLen / sizeof(wchar_t) - (size_t) (ptr - (const wchar_t*) regData));
188 ffStrbufInitNWS(FF_LIST_ADD(FFstrbuf, *list), strLen, ptr);
189 ptr += strLen;
190 }
191 break;
192 }
193
194 case FF_ARG_TYPE_BUFFER: {
195 FFArgBuffer* buffer = (FFArgBuffer*) arg->value;
196 if (buffer->length == 0) {
197 buffer->data = malloc(regDataLen);
198 } else if (buffer->length < regDataLen) {
199 if (error) {
200 FF_STRBUF_AUTO_DESTROY nameA = arg->name ? ffStrbufCreateWS(arg->name) : ffStrbufCreateStatic("(default)");
201 ffStrbufAppendF(error, "ffRegReadValues(%s) buffer too small (%u): expected %u", nameA.chars, (unsigned) buffer->length, (unsigned) regDataLen);
202 }
203 return false;
204 }
205 buffer->length = regDataLen;
206 memcpy(buffer->data, regData, regDataLen);
207 break;
208 }
209
210 case FF_ARG_TYPE_INT: // Use UINT instead
211 case FF_ARG_TYPE_STRING:
212 case FF_ARG_TYPE_NULL:
213 default:
214 if (error) {
215 FF_STRBUF_AUTO_DESTROY nameA = arg->name ? ffStrbufCreateWS(arg->name) : ffStrbufCreateStatic("(default)");
216 ffStrbufAppendF(error, "processRegValue(%s) unsupported FFArgType %u", nameA.chars, (unsigned) arg->type);
217 }
218 return false;
219 }
220
221 return true;
222
223type_mismatch:
224 FF_DEBUG("ffRegReadValues(%ls) type mismatch: regType=%u, argType=%u, dataLen=%u",
225 arg->name ?: L"(default)",
226 (unsigned) regType,
227 (unsigned) arg->type,
228 (unsigned) regDataLen);
229 if (error) {
230 FF_STRBUF_AUTO_DESTROY nameA = arg->name ? ffStrbufCreateWS(arg->name) : ffStrbufCreateStatic("(default)");
231 ffStrbufAppendF(error, "ffRegReadValues(%s) type mismatch: regType=%u, argType=%u, dataLen=%u", nameA.chars, (unsigned) regType, (unsigned) arg->type, (unsigned) regDataLen);
232 }
233 return false;
234}
235
236bool ffRegReadValue(HANDLE hKey, const FFRegValueArg* arg, FFstrbuf* error) {
237 UNICODE_STRING* valueNameU = &(UNICODE_STRING) {
238 .Length = arg->name ? (USHORT) (wcslen(arg->name) * sizeof(wchar_t)) : 0 /*(default)*/,
239 .MaximumLength = 0,
240 .Buffer = (wchar_t*) arg->name,
241 };
242
243 alignas(KEY_VALUE_PARTIAL_INFORMATION) uint8_t staticBuffer[128 + sizeof(KEY_VALUE_PARTIAL_INFORMATION)];
244 FF_AUTO_FREE uint8_t* dynamicBuffer = nullptr;
245
246 KEY_VALUE_PARTIAL_INFORMATION* buffer = (KEY_VALUE_PARTIAL_INFORMATION*) &staticBuffer;
247 DWORD bufSize = sizeof(staticBuffer);
248 if (NT_SUCCESS(NtQueryValueKey(hKey, valueNameU, KeyValuePartialInformation, buffer, bufSize, &bufSize))) {
249 goto process_value;
250 }
251
252 if (bufSize == 0) {
253 FF_DEBUG("NtQueryValueKey(%p, %ls) failed (bufSize=0)", hKey, arg->name ?: L"(default)");
254 if (error) {
255 FF_STRBUF_AUTO_DESTROY valueNameA = arg->name ? ffStrbufCreateWS(arg->name) : ffStrbufCreateStatic("(default)");
256 ffStrbufAppendF(error, "NtQueryValueKey(%p, %s) failed", hKey, valueNameA.chars);
257 }
258 return false;
259 }
260
261 dynamicBuffer = (uint8_t*) malloc(bufSize);
262 buffer = (KEY_VALUE_PARTIAL_INFORMATION*) dynamicBuffer;
263
264 if (!NT_SUCCESS(NtQueryValueKey(hKey, valueNameU, KeyValuePartialInformation, buffer, bufSize, &bufSize))) {
265 FF_DEBUG("NtQueryValueKey(%p, %ls, buffer=%u) failed", hKey, arg->name ?: L"(default)", (unsigned) bufSize);
266 if (error) {
267 FF_STRBUF_AUTO_DESTROY valueNameA = arg->name ? ffStrbufCreateWS(arg->name) : ffStrbufCreateStatic("(default)");
268 ffStrbufAppendF(error, "NtQueryValueKey(%p, %s, buffer) failed", hKey, valueNameA.chars);
269 }
270 return false;
271 }
272
273process_value:
274 FF_DEBUG("Read value from %p (%ls), type=%u, len=%u", hKey, arg->name ?: L"(default)", (unsigned) buffer->Type, (unsigned) buffer->DataLength);
275 return processRegValue(arg, buffer->Type, buffer->Data, buffer->DataLength, error);
276}
277
278bool ffRegReadValues(HANDLE hKey, uint32_t argc, const FFRegValueArg argv[], FFstrbuf* error) {
279 if (__builtin_expect(argc == 0, false)) {
280 return true;
281 }
282
283 assert(argv);
284
285 FF_AUTO_FREE UNICODE_STRING* names = (UNICODE_STRING*) calloc(argc, sizeof(*names));
286 FF_AUTO_FREE KEY_VALUE_ENTRY* entries = (KEY_VALUE_ENTRY*) calloc(argc, sizeof(*entries));
287
288 for (uint32_t i = 0; i < argc; ++i) {
289 if (__builtin_expect(!argv[i].value, false)) {
290 FF_DEBUG("ffRegReadValues(argv[%u].value) is nullptr", (unsigned) i);
291 if (error) {
292 ffStrbufAppendF(error, "ffRegReadValues(argv[%u].pVar) is nullptr", (unsigned) i);
293 }
294 return false;
295 }
296
297 names[i] = (UNICODE_STRING) {
298 .Length = argv[i].name ? (USHORT) (wcslen(argv[i].name) * sizeof(wchar_t)) : 0 /*(default)*/,
299 .MaximumLength = 0,
300 .Buffer = (wchar_t*) argv[i].name,
301 };
302 entries[i].ValueName = &names[i];
303 }
304
305 ULONG bufferSize = argc * 128;
306 if (bufferSize < 512) {
307 bufferSize = 512;
308 }
309
310 FF_AUTO_FREE uint8_t* buffer = nullptr;
311
312 while (true) {
313 buffer = (uint8_t*) realloc(buffer, bufferSize);
314
315 ULONG writtenSize = bufferSize;
316 ULONG requiredSize = 0;
317 NTSTATUS status = NtQueryMultipleValueKey(hKey, entries, argc, buffer, &writtenSize, &requiredSize);
318
319 if (!NT_SUCCESS(status)) {
320 // Buffer too small: docs guarantee requiredSize is returned when provided.
321 if (requiredSize > bufferSize) {
322 FF_DEBUG("NtQueryMultipleValueKey(%p) resize buffer: %u -> %u", hKey, (unsigned) bufferSize, (unsigned) requiredSize);
323 bufferSize = requiredSize;
324 continue;
325 }
326
327 FF_DEBUG("NtQueryMultipleValueKey(%p, argc=%u) failed, status=0x%08X", hKey, (unsigned) argc, (unsigned) status);
328 if (error) {
329 ffStrbufAppendF(error, "NtQueryMultipleValueKey(%p, argc=%u) failed, status=0x%08X", hKey, (unsigned) argc, (unsigned) status);
330 }
331 return false;
332 }
333
334 break;
335 }
336
337 for (uint32_t i = 0; i < argc; ++i) {
338 const FFRegValueArg* arg = &argv[i];
339 const KEY_VALUE_ENTRY* entry = &entries[i];
340
341 FF_DEBUG("Read value[%u] from %p: type=%u, len=%u", (unsigned) i, hKey, (unsigned) entry->Type, (unsigned) entry->DataLength);
342 if (!processRegValue(arg, entry->Type, buffer + entry->DataOffset, entry->DataLength, error)) {
343 return false;
344 }
345 }
346
347 return true;
348}
349
350bool ffRegGetSubKey(HANDLE hKey, uint32_t index, FFstrbuf* result, FFstrbuf* error) {
351 assert(hKey);
352 assert(result);
353
354 alignas(KEY_BASIC_INFORMATION) uint8_t buffer[sizeof(KEY_BASIC_INFORMATION) + MAX_PATH * sizeof(wchar_t)];
355 ULONG bufSize = (ULONG) sizeof(buffer);
356 KEY_BASIC_INFORMATION* keyInfo = (KEY_BASIC_INFORMATION*) buffer;
357
358 // Every failure mode fails NT_SUCCESS, and the read below depends on that: on a too-small buffer
359 // the kernel returns STATUS_BUFFER_OVERFLOW, writes the *full* NameLength but copies only part of
360 // the name (measured), so treating that status as success would read past what was written.
361 // The longest name the CM accepts, 255 characters, still fits: 255 * 2 + 16 <= sizeof(buffer).
362 if (!NT_SUCCESS(NtEnumerateKey(hKey, index, KeyBasicInformation, keyInfo, bufSize, &bufSize))) {
363 FF_DEBUG("NtEnumerateKey(hKey=%p, index=%u) failed", hKey, (unsigned) index);
364 if (error) {
365 ffStrbufAppendF(error, "NtEnumerateKey(hKey=%p, %u, keyInfo) failed", hKey, (unsigned) index);
366 }
367 return false;
368 }
369
370 ffStrbufSetNWS(result, keyInfo->NameLength / sizeof(wchar_t), keyInfo->Name);
371 return true;
372}
373
374// Reads the counters the key object already caches. KeyFullInformation would return the same numbers
375// but also marshals the key's class string out; measured per query on the ARP and MSIX keys:
376// 837 ns vs 895 ns (interleaved, 200k queries each). The open dominates the total -- 22.10 us vs
377// 22.40 us for open + query -- so this is a small win, not a big one.
378//
379// The kernel writes exactly sizeof(KEY_CACHED_INFORMATION) bytes and rejects a smaller buffer with
380// STATUS_BUFFER_TOO_SMALL without writing anything, so pass a whole KEY_CACHED_INFORMATION.
381bool ffRegQueryKey(HANDLE hKey, KEY_CACHED_INFORMATION* result, FFstrbuf* error) {
382 assert(hKey);
383 assert(result);
384
385 ULONG bufSize = (ULONG) sizeof(*result);
386
387 if (!NT_SUCCESS(NtQueryKey(hKey, KeyCachedInformation, result, bufSize, &bufSize))) {
388 FF_DEBUG("NtQueryKey(hKey=%p, KeyCachedInformation) failed", hKey);
389 if (error) {
390 ffStrbufAppendF(error, "NtQueryKey(hKey=%p, KeyCachedInformation, keyInfo) failed", hKey);
391 }
392 return false;
393 }
394
395 return true;
396}