1 /*
2  * PROJECT:     ReactOS User API Server DLL
3  * LICENSE:     GPL-2.0+ (https://spdx.org/licenses/GPL-2.0+)
4  * PURPOSE:     Hard errors support.
5  * COPYRIGHT:   Copyright 2007-2018 Dmitry Philippov (shedon@mail.ru)
6  *              Copyright 2010-2018 Timo Kreuzer (timo.kreuzer@reactos.org)
7  *              Copyright 2012-2018 Hermes Belusca-Maito
8  *              Copyright 2018 Giannis Adamopoulos
9  */
10 
11 /* INCLUDES *******************************************************************/
12 
13 #include "usersrv.h"
14 
15 #define NTOS_MODE_USER
16 #include <ndk/mmfuncs.h>
17 
18 #include <undocelfapi.h>
19 #include <ntstrsafe.h>
20 
21 #include "resource.h"
22 
23 #define NDEBUG
24 #include <debug.h>
25 
26 
27 /* FUNCTIONS ******************************************************************/
28 
29 /* Cache for the localized hard-error message box strings */
30 LANGID g_CurrentUserLangId = 0;
31 UNICODE_STRING g_SuccessU = {0, 0, NULL};
32 UNICODE_STRING g_InformationU = {0, 0, NULL};
33 UNICODE_STRING g_WarningU = {0, 0, NULL};
34 UNICODE_STRING g_ErrorU = {0, 0, NULL};
35 UNICODE_STRING g_SystemProcessU = {0, 0, NULL};
36 UNICODE_STRING g_OKTerminateU = {0, 0, NULL};
37 UNICODE_STRING g_CancelDebugU = {0, 0, NULL};
38 
39 VOID
40 RtlLoadUnicodeString(
41     IN HINSTANCE hInstance OPTIONAL,
42     IN UINT uID,
43     OUT PUNICODE_STRING pUnicodeString,
44     IN PCWSTR pDefaultString)
45 {
46     UINT Length;
47 
48     /* Try to load the string from the resource */
49     Length = LoadStringW(hInstance, uID, (LPWSTR)&pUnicodeString->Buffer, 0);
50     if (Length == 0)
51     {
52         /* If the resource string was not found, use the fallback default one */
53         RtlInitUnicodeString(pUnicodeString, pDefaultString);
54     }
55     else
56     {
57         /* Set the string length (not NULL-terminated!) */
58         pUnicodeString->MaximumLength = (USHORT)(Length * sizeof(WCHAR));
59         pUnicodeString->Length = pUnicodeString->MaximumLength;
60     }
61 }
62 
63 
64 /*
65  * NOTE: _scwprintf() is NOT exported by ntdll.dll,
66  * only _vscwprintf() is, so we need to implement it here.
67  * Code comes from sdk/lib/crt/printf/_scwprintf.c .
68  */
69 int
70 __cdecl
71 _scwprintf(
72     const wchar_t *format,
73     ...)
74 {
75     int len;
76     va_list args;
77 
78     va_start(args, format);
79     len = _vscwprintf(format, args);
80     va_end(args);
81 
82     return len;
83 }
84 
85 
86 /* FIXME */
87 int
88 WINAPI
89 MessageBoxTimeoutW(
90     HWND hWnd,
91     LPCWSTR lpText,
92     LPCWSTR lpCaption,
93     UINT uType,
94     WORD wLanguageId,
95     DWORD dwTime);
96 
97 
98 static
99 VOID
100 UserpCaptureStringParameters(
101     OUT PULONG_PTR Parameters,
102     OUT PULONG SizeOfAllUnicodeStrings,
103     IN PHARDERROR_MSG Message,
104     IN HANDLE hProcess OPTIONAL)
105 {
106     NTSTATUS Status;
107     ULONG nParam, Size = 0;
108     UNICODE_STRING TempStringU, ParamStringU;
109     ANSI_STRING TempStringA;
110 
111     if (SizeOfAllUnicodeStrings)
112         *SizeOfAllUnicodeStrings = 0;
113 
114     /* Read all strings from client space */
115     for (nParam = 0; nParam < Message->NumberOfParameters; ++nParam)
116     {
117         Parameters[nParam] = 0;
118 
119         /* Check if the current parameter is a unicode string */
120         if (Message->UnicodeStringParameterMask & (1 << nParam))
121         {
122             /* Skip this string if we do not have a client process */
123             if (!hProcess)
124                 continue;
125 
126             /* Read the UNICODE_STRING from the process memory */
127             Status = NtReadVirtualMemory(hProcess,
128                                          (PVOID)Message->Parameters[nParam],
129                                          &ParamStringU,
130                                          sizeof(ParamStringU),
131                                          NULL);
132             if (!NT_SUCCESS(Status))
133             {
134                 /* We failed, skip this string */
135                 DPRINT1("NtReadVirtualMemory(Message->Parameters) failed, Status 0x%lx, skipping.\n", Status);
136                 continue;
137             }
138 
139             /* Allocate a buffer for the string and reserve a NULL terminator */
140             TempStringU.MaximumLength = ParamStringU.Length + sizeof(UNICODE_NULL);
141             TempStringU.Length = ParamStringU.Length;
142             TempStringU.Buffer = RtlAllocateHeap(RtlGetProcessHeap(),
143                                                  HEAP_ZERO_MEMORY,
144                                                  TempStringU.MaximumLength);
145             if (!TempStringU.Buffer)
146             {
147                 /* We failed, skip this string */
148                 DPRINT1("Cannot allocate memory with size %u, skipping.\n", TempStringU.MaximumLength);
149                 continue;
150             }
151 
152             /* Read the string buffer from the process memory */
153             Status = NtReadVirtualMemory(hProcess,
154                                          ParamStringU.Buffer,
155                                          TempStringU.Buffer,
156                                          ParamStringU.Length,
157                                          NULL);
158             if (!NT_SUCCESS(Status))
159             {
160                 /* We failed, skip this string */
161                 DPRINT1("NtReadVirtualMemory(ParamStringU) failed, Status 0x%lx, skipping.\n", Status);
162                 RtlFreeHeap(RtlGetProcessHeap(), 0, TempStringU.Buffer);
163                 continue;
164             }
165             /* NULL-terminate the string */
166             TempStringU.Buffer[TempStringU.Length / sizeof(WCHAR)] = UNICODE_NULL;
167 
168             DPRINT("ParamString = \'%wZ\'\n", &TempStringU);
169 
170             if (Message->Status == STATUS_SERVICE_NOTIFICATION)
171             {
172                 /* Just keep the allocated NULL-terminated UNICODE string */
173                 Parameters[nParam] = (ULONG_PTR)TempStringU.Buffer;
174                 Size += TempStringU.Length;
175             }
176             else
177             {
178                 /* Allocate a buffer for conversion to ANSI string */
179                 TempStringA.MaximumLength = (USHORT)RtlUnicodeStringToAnsiSize(&TempStringU);
180                 TempStringA.Buffer = RtlAllocateHeap(RtlGetProcessHeap(),
181                                                      HEAP_ZERO_MEMORY,
182                                                      TempStringA.MaximumLength);
183                 if (!TempStringA.Buffer)
184                 {
185                     /* We failed, skip this string */
186                     DPRINT1("Cannot allocate memory with size %u, skipping.\n", TempStringA.MaximumLength);
187                     RtlFreeHeap(RtlGetProcessHeap(), 0, TempStringU.Buffer);
188                     continue;
189                 }
190 
191                 /* Convert string to ANSI and free temporary buffer */
192                 Status = RtlUnicodeStringToAnsiString(&TempStringA, &TempStringU, FALSE);
193                 RtlFreeHeap(RtlGetProcessHeap(), 0, TempStringU.Buffer);
194                 if (!NT_SUCCESS(Status))
195                 {
196                     /* We failed, skip this string */
197                     DPRINT1("RtlUnicodeStringToAnsiString() failed, Status 0x%lx, skipping.\n", Status);
198                     RtlFreeHeap(RtlGetProcessHeap(), 0, TempStringA.Buffer);
199                     continue;
200                 }
201 
202                 /* Note: RtlUnicodeStringToAnsiString() returns a NULL-terminated string */
203                 Parameters[nParam] = (ULONG_PTR)TempStringA.Buffer;
204                 Size += TempStringU.Length;
205             }
206         }
207         else
208         {
209             /* It's not a unicode string, just copy the parameter */
210             Parameters[nParam] = Message->Parameters[nParam];
211         }
212     }
213 
214     if (SizeOfAllUnicodeStrings)
215         *SizeOfAllUnicodeStrings = Size;
216 }
217 
218 static
219 VOID
220 UserpFreeStringParameters(
221     IN OUT PULONG_PTR Parameters,
222     IN PHARDERROR_MSG Message)
223 {
224     ULONG nParam;
225 
226     /* Loop all parameters */
227     for (nParam = 0; nParam < Message->NumberOfParameters; ++nParam)
228     {
229         /* Check if the current parameter is a string */
230         if ((Message->UnicodeStringParameterMask & (1 << nParam)) && (Parameters[nParam] != 0))
231         {
232             /* Free the string buffer */
233             RtlFreeHeap(RtlGetProcessHeap(), 0, (PVOID)Parameters[nParam]);
234         }
235     }
236 }
237 
238 static
239 NTSTATUS
240 UserpGetClientFileName(
241     OUT PUNICODE_STRING ClientFileNameU,
242     IN HANDLE hProcess)
243 {
244     PLIST_ENTRY ModuleListHead;
245     PLIST_ENTRY Entry;
246     PLDR_DATA_TABLE_ENTRY Module;
247     PPEB_LDR_DATA Ldr;
248     PROCESS_BASIC_INFORMATION ClientBasicInfo;
249     LDR_DATA_TABLE_ENTRY ModuleData;
250     PVOID ClientDllBase;
251     NTSTATUS Status;
252     PPEB Peb;
253 
254     /* Initialize string */
255     RtlInitEmptyUnicodeString(ClientFileNameU, NULL, 0);
256 
257     /* Query process information */
258     Status = NtQueryInformationProcess(hProcess,
259                                        ProcessBasicInformation,
260                                        &ClientBasicInfo,
261                                        sizeof(ClientBasicInfo),
262                                        NULL);
263     if (!NT_SUCCESS(Status)) return Status;
264 
265     /* Locate the process loader data table and retrieve its name from it */
266 
267     Peb = ClientBasicInfo.PebBaseAddress;
268     if (!Peb) return STATUS_UNSUCCESSFUL;
269 
270     Status = NtReadVirtualMemory(hProcess, &Peb->Ldr, &Ldr, sizeof(Ldr), NULL);
271     if (!NT_SUCCESS(Status)) return Status;
272 
273     ModuleListHead = &Ldr->InLoadOrderModuleList;
274     Status = NtReadVirtualMemory(hProcess,
275                                  &ModuleListHead->Flink,
276                                  &Entry,
277                                  sizeof(Entry),
278                                  NULL);
279     if (!NT_SUCCESS(Status)) return Status;
280 
281     if (Entry == ModuleListHead) return STATUS_UNSUCCESSFUL;
282 
283     Module = CONTAINING_RECORD(Entry, LDR_DATA_TABLE_ENTRY, InLoadOrderLinks);
284 
285     Status = NtReadVirtualMemory(hProcess,
286                                  Module,
287                                  &ModuleData,
288                                  sizeof(ModuleData),
289                                  NULL);
290     if (!NT_SUCCESS(Status)) return Status;
291 
292     Status = NtReadVirtualMemory(hProcess,
293                                  &Peb->ImageBaseAddress,
294                                  &ClientDllBase,
295                                  sizeof(ClientDllBase),
296                                  NULL);
297     if (!NT_SUCCESS(Status)) return Status;
298 
299     if (ClientDllBase != ModuleData.DllBase) return STATUS_UNSUCCESSFUL;
300 
301     ClientFileNameU->MaximumLength = ModuleData.BaseDllName.MaximumLength;
302     ClientFileNameU->Buffer = RtlAllocateHeap(RtlGetProcessHeap(),
303                                               HEAP_ZERO_MEMORY,
304                                               ClientFileNameU->MaximumLength);
305     if (!ClientFileNameU->Buffer)
306     {
307         RtlInitEmptyUnicodeString(ClientFileNameU, NULL, 0);
308         return STATUS_NO_MEMORY;
309     }
310 
311     Status = NtReadVirtualMemory(hProcess,
312                                  ModuleData.BaseDllName.Buffer,
313                                  ClientFileNameU->Buffer,
314                                  ClientFileNameU->MaximumLength,
315                                  NULL);
316     if (!NT_SUCCESS(Status))
317     {
318         RtlFreeHeap(RtlGetProcessHeap(), 0, ClientFileNameU->Buffer);
319         RtlInitEmptyUnicodeString(ClientFileNameU, NULL, 0);
320         return Status;
321     }
322 
323     ClientFileNameU->Length = (USHORT)(wcslen(ClientFileNameU->Buffer) * sizeof(WCHAR));
324     DPRINT("ClientFileNameU = \'%wZ\'\n", &ClientFileNameU);
325 
326     return STATUS_SUCCESS;
327 }
328 
329 static
330 VOID
331 UserpDuplicateParamStringToUnicodeString(
332     IN OUT PUNICODE_STRING UnicodeString,
333     IN PCWSTR ParamString)
334 {
335     UNICODE_STRING FormatU, TempStringU;
336 
337     /* Calculate buffer length for the text message */
338     RtlInitUnicodeString(&FormatU, (PWSTR)ParamString);
339     if (UnicodeString->MaximumLength < FormatU.MaximumLength)
340     {
341         /* Duplicate the text message in a larger buffer */
342         if (NT_SUCCESS(RtlDuplicateUnicodeString(RTL_DUPLICATE_UNICODE_STRING_NULL_TERMINATE,
343                                                  &FormatU, &TempStringU)))
344         {
345             *UnicodeString = TempStringU;
346         }
347         else
348         {
349             /* We could not allocate a larger buffer; continue using the smaller original buffer */
350             DPRINT1("Cannot allocate memory for UnicodeString, use original buffer.\n");
351 
352             /* Copy the truncated string, NULL-terminate it */
353             FormatU.MaximumLength = UnicodeString->MaximumLength;
354             FormatU.Length = FormatU.MaximumLength - sizeof(UNICODE_NULL);
355             RtlCopyUnicodeString(UnicodeString, &FormatU);
356         }
357     }
358     else
359     {
360         /* Copy the string, NULL-terminate it */
361         RtlCopyUnicodeString(UnicodeString, &FormatU);
362     }
363 }
364 
365 static
366 VOID
367 UserpFormatMessages(
368     IN OUT PUNICODE_STRING TextStringU,
369     IN OUT PUNICODE_STRING CaptionStringU,
370     OUT PUINT pdwType,
371     OUT PULONG pdwTimeout,
372     IN PHARDERROR_MSG Message)
373 {
374     /* Special hardcoded messages */
375     static const PCWSTR pszUnknownHardError =
376         L"Unknown Hard Error 0x%08lx\n"
377         L"Parameters: 0x%p 0x%p 0x%p 0x%p";
378     static const PCWSTR pszExceptionHardError =
379         L"Exception processing message 0x%08lx\n"
380         L"Parameters: 0x%p 0x%p 0x%p 0x%p";
381 
382     NTSTATUS Status;
383     OBJECT_ATTRIBUTES ObjectAttributes;
384     HANDLE hProcess;
385     ULONG Severity = (ULONG)(Message->Status) >> 30;
386     ULONG SizeOfStrings;
387     ULONG_PTR Parameters[MAXIMUM_HARDERROR_PARAMETERS] = {0};
388     ULONG_PTR CopyParameters[MAXIMUM_HARDERROR_PARAMETERS];
389     UNICODE_STRING WindowTitleU, FileNameU, TempStringU, FormatU, Format2U;
390     ANSI_STRING FormatA, Format2A;
391     HWND hwndOwner;
392     PMESSAGE_RESOURCE_ENTRY MessageResource;
393     PWSTR FormatString, pszBuffer;
394     size_t cszBuffer;
395 
396     /* Open client process */
397     InitializeObjectAttributes(&ObjectAttributes, NULL, 0, NULL, NULL);
398     Status = NtOpenProcess(&hProcess,
399                            PROCESS_VM_READ | PROCESS_QUERY_INFORMATION,
400                            &ObjectAttributes,
401                            &Message->h.ClientId);
402     if (!NT_SUCCESS(Status))
403     {
404         DPRINT1("NtOpenProcess failed with status 0x%08lx, possibly SYSTEM process.\n", Status);
405         hProcess = NULL;
406     }
407 
408     /* Capture all string parameters from the process memory */
409     UserpCaptureStringParameters(Parameters, &SizeOfStrings, Message, hProcess);
410 
411     /* Initialize the output strings */
412     TextStringU->Length = 0;
413     TextStringU->Buffer[0] = UNICODE_NULL;
414 
415     CaptionStringU->Length = 0;
416     CaptionStringU->Buffer[0] = UNICODE_NULL;
417 
418     /*
419      * Check whether it is a service notification, in which case
420      * we format the parameters and take the short route.
421      */
422     if (Message->Status == STATUS_SERVICE_NOTIFICATION)
423     {
424         /* Close the process handle */
425         if (hProcess) NtClose(hProcess);
426 
427         /*
428          * Retrieve the message box flags. Note that we filter out
429          * MB_SERVICE_NOTIFICATION to not enter an infinite recursive
430          * loop when we will call MessageBox() later on.
431          */
432         *pdwType = (UINT)Parameters[2] & ~MB_SERVICE_NOTIFICATION;
433 
434         /*
435          * Duplicate the UNICODE text message and caption.
436          * If no strings or invalid ones have been provided, keep
437          * the original buffers and reset the string lengths to zero.
438          */
439         if (Message->UnicodeStringParameterMask & 0x1)
440             UserpDuplicateParamStringToUnicodeString(TextStringU, (PCWSTR)Parameters[0]);
441         if (Message->UnicodeStringParameterMask & 0x2)
442             UserpDuplicateParamStringToUnicodeString(CaptionStringU, (PCWSTR)Parameters[1]);
443 
444         /* Set the timeout */
445         if (Message->NumberOfParameters >= 4)
446             *pdwTimeout = (ULONG)Parameters[3];
447         else
448             *pdwTimeout = INFINITE;
449 
450         goto Quit;
451     }
452 
453     /* Set the message box type */
454     *pdwType = 0;
455     switch (Message->ValidResponseOptions)
456     {
457         case OptionAbortRetryIgnore:
458             *pdwType = MB_ABORTRETRYIGNORE;
459             break;
460         case OptionOk:
461             *pdwType = MB_OK;
462             break;
463         case OptionOkCancel:
464             *pdwType = MB_OKCANCEL;
465             break;
466         case OptionRetryCancel:
467             *pdwType = MB_RETRYCANCEL;
468             break;
469         case OptionYesNo:
470             *pdwType = MB_YESNO;
471             break;
472         case OptionYesNoCancel:
473             *pdwType = MB_YESNOCANCEL;
474             break;
475         case OptionShutdownSystem:
476             *pdwType = MB_OK;
477             break;
478         case OptionOkNoWait:
479             *pdwType = MB_OK;
480             break;
481         case OptionCancelTryContinue:
482             *pdwType = MB_CANCELTRYCONTINUE;
483             break;
484     }
485 
486     /* Set the severity icon */
487     // STATUS_SEVERITY_SUCCESS
488          if (Severity == STATUS_SEVERITY_INFORMATIONAL) *pdwType |= MB_ICONINFORMATION;
489     else if (Severity == STATUS_SEVERITY_WARNING)       *pdwType |= MB_ICONWARNING;
490     else if (Severity == STATUS_SEVERITY_ERROR)         *pdwType |= MB_ICONERROR;
491 
492     *pdwType |= MB_SYSTEMMODAL | MB_SETFOREGROUND;
493 
494     /* Set the timeout */
495     *pdwTimeout = INFINITE;
496 
497     /* Copy the Parameters array locally */
498     RtlCopyMemory(&CopyParameters, Parameters, sizeof(CopyParameters));
499 
500     /* Get the file name of the client process */
501     Status = STATUS_SUCCESS;
502     if (hProcess)
503         Status = UserpGetClientFileName(&FileNameU, hProcess);
504 
505     /* Close the process handle but keep its original value to know where stuff came from */
506     if (hProcess) NtClose(hProcess);
507 
508     /*
509      * Fall back to SYSTEM process if the client process handle
510      * was NULL or we failed retrieving a file name.
511      */
512     if (!hProcess || !NT_SUCCESS(Status) || !FileNameU.Buffer)
513     {
514         hProcess  = NULL;
515         FileNameU = g_SystemProcessU;
516     }
517 
518     /* Retrieve the description of the error code */
519     FormatA.Buffer = NULL;
520     Status = RtlFindMessage(GetModuleHandleW(L"ntdll"),
521                             (ULONG_PTR)RT_MESSAGETABLE,
522                             LANG_NEUTRAL,
523                             Message->Status,
524                             &MessageResource);
525     if (NT_SUCCESS(Status))
526     {
527         if (MessageResource->Flags)
528         {
529             RtlInitUnicodeString(&FormatU, (PWSTR)MessageResource->Text);
530             FormatA.Buffer = NULL;
531         }
532         else
533         {
534             RtlInitAnsiString(&FormatA, (PSTR)MessageResource->Text);
535             /* Status = */ RtlAnsiStringToUnicodeString(&FormatU, &FormatA, TRUE);
536         }
537         ASSERT(FormatU.Buffer);
538     }
539     else
540     {
541         /*
542          * Fall back to unknown hard error format string.
543          * NOTE: The value used here is ReactOS-specific: it allows specifying
544          * the exact hard error status value and the parameters, contrary to
545          * the one on Windows that only says: "Unknown Hard Error".
546          */
547         RtlInitEmptyUnicodeString(&FormatU, NULL, 0);
548         FormatA.Buffer = NULL;
549     }
550 
551     FormatString = FormatU.Buffer;
552 
553     /* Check whether a caption is specified in the format string */
554     if (FormatString && FormatString[0] == L'{')
555     {
556         /* Set caption start */
557         TempStringU.Buffer = ++FormatString;
558 
559         /* Get the caption size and find where the format string really starts */
560         for (TempStringU.Length = 0;
561              *FormatString != UNICODE_NULL && *FormatString != L'}';
562              ++TempStringU.Length)
563         {
564             ++FormatString;
565         }
566 
567         /* Skip '}', '\r', '\n' */
568         FormatString += 3;
569 
570         TempStringU.Length *= sizeof(WCHAR);
571         TempStringU.MaximumLength = TempStringU.Length;
572     }
573     else
574     {
575         if (Severity == STATUS_SEVERITY_SUCCESS)
576             TempStringU = g_SuccessU;
577         else if (Severity == STATUS_SEVERITY_INFORMATIONAL)
578             TempStringU = g_InformationU;
579         else if (Severity == STATUS_SEVERITY_WARNING)
580             TempStringU = g_WarningU;
581         else if (Severity == STATUS_SEVERITY_ERROR)
582             TempStringU = g_ErrorU;
583         else
584             ASSERT(FALSE); // Unexpected, since Severity is only <= 3.
585     }
586 
587     /* Retrieve the window title of the client, if it has one */
588     RtlInitEmptyUnicodeString(&WindowTitleU, L"", 0);
589     hwndOwner = NULL;
590     EnumThreadWindows(HandleToUlong(Message->h.ClientId.UniqueThread),
591                       FindTopLevelWnd, (LPARAM)&hwndOwner);
592     if (hwndOwner)
593     {
594         cszBuffer = GetWindowTextLengthW(hwndOwner);
595         if (cszBuffer != 0)
596         {
597             cszBuffer += 3; // 2 characters for ": " and a NULL terminator.
598             cszBuffer *= sizeof(WCHAR);
599             pszBuffer = RtlAllocateHeap(RtlGetProcessHeap(),
600                                         HEAP_ZERO_MEMORY,
601                                         cszBuffer);
602             if (pszBuffer)
603             {
604                 RtlInitEmptyUnicodeString(&WindowTitleU, pszBuffer, (USHORT)cszBuffer);
605                 cszBuffer = GetWindowTextW(hwndOwner,
606                                            WindowTitleU.Buffer,
607                                            WindowTitleU.MaximumLength / sizeof(WCHAR));
608                 WindowTitleU.Length = (USHORT)(cszBuffer * sizeof(WCHAR));
609                 RtlAppendUnicodeToString(&WindowTitleU, L": ");
610             }
611         }
612     }
613 
614     /* Calculate buffer length for the caption */
615     cszBuffer = WindowTitleU.Length + FileNameU.Length + TempStringU.Length +
616                 3 * sizeof(WCHAR) + sizeof(UNICODE_NULL);
617     if (CaptionStringU->MaximumLength < cszBuffer)
618     {
619         /* Allocate a larger buffer for the caption */
620         pszBuffer = RtlAllocateHeap(RtlGetProcessHeap(),
621                                     HEAP_ZERO_MEMORY,
622                                     cszBuffer);
623         if (!pszBuffer)
624         {
625             /* We could not allocate a larger buffer; continue using the smaller original buffer */
626             DPRINT1("Cannot allocate memory for CaptionStringU, use original buffer.\n");
627         }
628         else
629         {
630             RtlInitEmptyUnicodeString(CaptionStringU, pszBuffer, (USHORT)cszBuffer);
631         }
632     }
633     CaptionStringU->Length = 0;
634     CaptionStringU->Buffer[0] = UNICODE_NULL;
635 
636     /* Build the caption */
637     RtlStringCbPrintfW(CaptionStringU->Buffer,
638                        CaptionStringU->MaximumLength,
639                        L"%wZ%wZ - %wZ",
640                        &WindowTitleU, &FileNameU, &TempStringU);
641     CaptionStringU->Length = (USHORT)(wcslen(CaptionStringU->Buffer) * sizeof(WCHAR));
642 
643     /* Free the strings if needed */
644     if (WindowTitleU.Buffer && (WindowTitleU.MaximumLength != 0))
645         RtlFreeUnicodeString(&WindowTitleU);
646     if (hProcess)
647         RtlFreeUnicodeString(&FileNameU);
648 
649     Format2A.Buffer = NULL;
650 
651     /* If we have an unknown hard error, skip the special cases handling */
652     if (!FormatString)
653         goto BuildMessage;
654 
655     /* Check if this is an exception message */
656     if (Message->Status == STATUS_UNHANDLED_EXCEPTION)
657     {
658         ULONG ExceptionCode = CopyParameters[0];
659 
660         /* Retrieve the description of the exception code */
661         Status = RtlFindMessage(GetModuleHandleW(L"ntdll"),
662                                 (ULONG_PTR)RT_MESSAGETABLE,
663                                 LANG_NEUTRAL,
664                                 ExceptionCode,
665                                 &MessageResource);
666         if (NT_SUCCESS(Status))
667         {
668             if (MessageResource->Flags)
669             {
670                 RtlInitUnicodeString(&Format2U, (PWSTR)MessageResource->Text);
671                 Format2A.Buffer = NULL;
672             }
673             else
674             {
675                 RtlInitAnsiString(&Format2A, (PSTR)MessageResource->Text);
676                 /* Status = */ RtlAnsiStringToUnicodeString(&Format2U, &Format2A, TRUE);
677             }
678             ASSERT(Format2U.Buffer);
679 
680             /* Handle special cases */
681             if (ExceptionCode == STATUS_ACCESS_VIOLATION)
682             {
683                 /* Use a new FormatString */
684                 FormatString = Format2U.Buffer;
685                 CopyParameters[0] = CopyParameters[1];
686                 CopyParameters[1] = CopyParameters[3];
687                 if (CopyParameters[2])
688                     CopyParameters[2] = (ULONG_PTR)L"written";
689                 else
690                     CopyParameters[2] = (ULONG_PTR)L"read";
691             }
692             else if (ExceptionCode == STATUS_IN_PAGE_ERROR)
693             {
694                 /* Use a new FormatString */
695                 FormatString = Format2U.Buffer;
696                 CopyParameters[0] = CopyParameters[1];
697                 CopyParameters[1] = CopyParameters[3];
698             }
699             else
700             {
701                 /* Keep the existing FormatString */
702                 CopyParameters[2] = CopyParameters[1];
703                 CopyParameters[1] = CopyParameters[0];
704 
705                 pszBuffer = Format2U.Buffer;
706                 if (!_wcsnicmp(pszBuffer, L"{EXCEPTION}", 11))
707                 {
708                     /*
709                      * This is a named exception. Skip the mark and
710                      * retrieve the exception name that follows it.
711                      */
712                     pszBuffer += 11;
713 
714                     /* Skip '\r', '\n' */
715                     pszBuffer += 2;
716 
717                     CopyParameters[0] = (ULONG_PTR)pszBuffer;
718                 }
719                 else
720                 {
721                     /* Fall back to hardcoded value */
722                     CopyParameters[0] = (ULONG_PTR)L"unknown software exception";
723                 }
724             }
725         }
726         else
727         {
728             /* Fall back to hardcoded value, and keep the existing FormatString */
729             CopyParameters[2] = CopyParameters[1];
730             CopyParameters[1] = CopyParameters[0];
731             CopyParameters[0] = (ULONG_PTR)L"unknown software exception";
732         }
733     }
734 
735 BuildMessage:
736     /*
737      * Calculate buffer length for the text message. If FormatString
738      * is NULL this means we have an unknown hard error whose format
739      * string is in FormatU.
740      */
741     cszBuffer = 0;
742     /* Wrap in SEH to protect from invalid string parameters */
743     _SEH2_TRY
744     {
745         if (!FormatString)
746         {
747             /* Fall back to unknown hard error format string, and use the original parameters */
748             cszBuffer = _scwprintf(pszUnknownHardError,
749                                    Message->Status,
750                                    Parameters[0], Parameters[1],
751                                    Parameters[2], Parameters[3]);
752             cszBuffer *= sizeof(WCHAR);
753         }
754         else
755         {
756             cszBuffer = _scwprintf(FormatString,
757                                    CopyParameters[0], CopyParameters[1],
758                                    CopyParameters[2], CopyParameters[3]);
759             cszBuffer *= sizeof(WCHAR);
760 
761             /* Add a description for the dialog buttons */
762             if (Message->Status == STATUS_UNHANDLED_EXCEPTION)
763             {
764                 if (Message->ValidResponseOptions == OptionOk ||
765                     Message->ValidResponseOptions == OptionOkCancel)
766                 {
767                     /* Reserve space for one newline and the OK-terminate-program string */
768                     cszBuffer += sizeof(WCHAR) + g_OKTerminateU.Length;
769                 }
770                 if (Message->ValidResponseOptions == OptionOkCancel)
771                 {
772                     /* Reserve space for one newline and the CANCEL-debug-program string */
773                     cszBuffer += sizeof(WCHAR) + g_CancelDebugU.Length;
774                 }
775             }
776         }
777     }
778     _SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
779     {
780         /* An exception occurred, use a default string with the original parameters */
781         cszBuffer = _scwprintf(pszExceptionHardError,
782                                Message->Status,
783                                Parameters[0], Parameters[1],
784                                Parameters[2], Parameters[3]);
785         cszBuffer *= sizeof(WCHAR);
786     }
787     _SEH2_END;
788 
789     cszBuffer += SizeOfStrings + sizeof(UNICODE_NULL);
790 
791     if (TextStringU->MaximumLength < cszBuffer)
792     {
793         /* Allocate a larger buffer for the text message */
794         pszBuffer = RtlAllocateHeap(RtlGetProcessHeap(),
795                                     HEAP_ZERO_MEMORY,
796                                     cszBuffer);
797         if (!pszBuffer)
798         {
799             /* We could not allocate a larger buffer; continue using the smaller original buffer */
800             DPRINT1("Cannot allocate memory for TextStringU, use original buffer.\n");
801         }
802         else
803         {
804             RtlInitEmptyUnicodeString(TextStringU, pszBuffer, (USHORT)cszBuffer);
805         }
806     }
807     TextStringU->Length = 0;
808     TextStringU->Buffer[0] = UNICODE_NULL;
809 
810     /* Wrap in SEH to protect from invalid string parameters */
811     _SEH2_TRY
812     {
813         /* Print the string into the buffer */
814         pszBuffer = TextStringU->Buffer;
815         cszBuffer = TextStringU->MaximumLength;
816 
817         if (!FormatString)
818         {
819             /* Fall back to unknown hard error format string, and use the original parameters */
820             RtlStringCbPrintfW(pszBuffer, cszBuffer,
821                                pszUnknownHardError,
822                                Message->Status,
823                                Parameters[0], Parameters[1],
824                                Parameters[2], Parameters[3]);
825         }
826         else
827         {
828             RtlStringCbPrintfExW(pszBuffer, cszBuffer,
829                                  &pszBuffer, &cszBuffer,
830                                  0,
831                                  FormatString,
832                                  CopyParameters[0], CopyParameters[1],
833                                  CopyParameters[2], CopyParameters[3]);
834 
835             /* Add a description for the dialog buttons */
836             if (Message->Status == STATUS_UNHANDLED_EXCEPTION)
837             {
838                 if (Message->ValidResponseOptions == OptionOk ||
839                     Message->ValidResponseOptions == OptionOkCancel)
840                 {
841                     RtlStringCbPrintfExW(pszBuffer, cszBuffer,
842                                          &pszBuffer, &cszBuffer,
843                                          0,
844                                          L"\n%wZ",
845                                          &g_OKTerminateU);
846                 }
847                 if (Message->ValidResponseOptions == OptionOkCancel)
848                 {
849                     RtlStringCbPrintfExW(pszBuffer, cszBuffer,
850                                          &pszBuffer, &cszBuffer,
851                                          0,
852                                          L"\n%wZ",
853                                          &g_CancelDebugU);
854                 }
855             }
856         }
857     }
858     _SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
859     {
860         /* An exception occurred, use a default string with the original parameters */
861         DPRINT1("Exception 0x%08lx occurred while building hard-error message, fall back to default message.\n",
862                 _SEH2_GetExceptionCode());
863 
864         RtlStringCbPrintfW(TextStringU->Buffer,
865                            TextStringU->MaximumLength,
866                            pszExceptionHardError,
867                            Message->Status,
868                            Parameters[0], Parameters[1],
869                            Parameters[2], Parameters[3]);
870     }
871     _SEH2_END;
872 
873     TextStringU->Length = (USHORT)(wcslen(TextStringU->Buffer) * sizeof(WCHAR));
874 
875     /* Free the converted UNICODE strings */
876     if (Format2A.Buffer) RtlFreeUnicodeString(&Format2U);
877     if (FormatA.Buffer) RtlFreeUnicodeString(&FormatU);
878 
879 Quit:
880     /* Free the captured parameters */
881     UserpFreeStringParameters(Parameters, Message);
882 }
883 
884 static ULONG
885 GetRegInt(
886     IN PCWSTR KeyName,
887     IN PCWSTR ValueName,
888     IN ULONG  DefaultValue)
889 {
890     NTSTATUS Status;
891     ULONG Value = DefaultValue;
892     UNICODE_STRING String;
893     OBJECT_ATTRIBUTES ObjectAttributes;
894     HANDLE KeyHandle;
895     ULONG ResultLength;
896     UCHAR ValueBuffer[sizeof(KEY_VALUE_PARTIAL_INFORMATION) + sizeof(ULONG)];
897     PKEY_VALUE_PARTIAL_INFORMATION ValueInfo = (PKEY_VALUE_PARTIAL_INFORMATION)ValueBuffer;
898 
899     RtlInitUnicodeString(&String, KeyName);
900     InitializeObjectAttributes(&ObjectAttributes,
901                                &String,
902                                OBJ_CASE_INSENSITIVE,
903                                NULL,
904                                NULL);
905 
906     /* Open the registry key */
907     Status = NtOpenKey(&KeyHandle, KEY_READ, &ObjectAttributes);
908     if (NT_SUCCESS(Status))
909     {
910         /* Query the value */
911         RtlInitUnicodeString(&String, ValueName);
912         Status = NtQueryValueKey(KeyHandle,
913                                  &String,
914                                  KeyValuePartialInformation,
915                                  ValueInfo,
916                                  sizeof(ValueBuffer),
917                                  &ResultLength);
918 
919         /* Close the registry key */
920         NtClose(KeyHandle);
921 
922         if (NT_SUCCESS(Status) && (ValueInfo->Type == REG_DWORD))
923         {
924             /* Directly retrieve the data */
925             Value = *(PULONG)ValueInfo->Data;
926         }
927     }
928 
929     return Value;
930 }
931 
932 static BOOL
933 UserpShowInformationBalloon(
934     IN PUNICODE_STRING TextStringU,
935     IN PUNICODE_STRING CaptionStringU,
936     IN UINT Type,
937     IN PHARDERROR_MSG Message)
938 {
939     ULONG ShellErrorMode;
940     HWND hWndTaskman;
941     COPYDATASTRUCT CopyData;
942     PBALLOON_HARD_ERROR_DATA pdata;
943     DWORD dwSize, cbTextLen, cbTitleLen;
944     PWCHAR pText, pCaption;
945     DWORD ret;
946     DWORD_PTR dwResult;
947 
948     /* Query the shell error mode value */
949     ShellErrorMode = GetRegInt(L"\\Registry\\Machine\\System\\CurrentControlSet\\Control\\Windows",
950                                L"ShellErrorMode", 0);
951 
952     /* Make the shell display the hard error message only if necessary */
953     if (ShellErrorMode != 1)
954         return FALSE;
955 
956     /* Retrieve the shell task window */
957     hWndTaskman = GetTaskmanWindow();
958     if (!hWndTaskman)
959     {
960         DPRINT1("Failed to find shell task window (last error %lu)\n", GetLastError());
961         return FALSE;
962     }
963 
964     cbTextLen  = TextStringU->Length + sizeof(UNICODE_NULL);
965     cbTitleLen = CaptionStringU->Length + sizeof(UNICODE_NULL);
966 
967     dwSize = sizeof(BALLOON_HARD_ERROR_DATA);
968     dwSize += cbTextLen + cbTitleLen;
969 
970     /* Build the data buffer */
971     pdata = RtlAllocateHeap(RtlGetProcessHeap(), HEAP_ZERO_MEMORY, dwSize);
972     if (!pdata)
973     {
974         DPRINT1("Failed to allocate balloon data\n");
975         return FALSE;
976     }
977 
978     pdata->cbHeaderSize = sizeof(BALLOON_HARD_ERROR_DATA);
979     pdata->Status = Message->Status;
980     pdata->dwType = Type;
981 
982     pdata->TitleOffset   = pdata->cbHeaderSize;
983     pdata->MessageOffset = pdata->TitleOffset + cbTitleLen;
984     pCaption = (PWCHAR)((ULONG_PTR)pdata + pdata->TitleOffset);
985     pText = (PWCHAR)((ULONG_PTR)pdata + pdata->MessageOffset);
986     RtlStringCbCopyNW(pCaption, cbTitleLen, CaptionStringU->Buffer, CaptionStringU->Length);
987     RtlStringCbCopyNW(pText, cbTextLen, TextStringU->Buffer, TextStringU->Length);
988 
989     /* Send the message */
990 
991     /* Retrieve a unique system-wide message to communicate hard error data with the shell */
992     CopyData.dwData = RegisterWindowMessageW(L"HardError");
993     CopyData.cbData = dwSize;
994     CopyData.lpData = pdata;
995 
996     dwResult = FALSE;
997     ret = SendMessageTimeoutW(hWndTaskman, WM_COPYDATA, 0, (LPARAM)&CopyData,
998                               SMTO_NORMAL | SMTO_ABORTIFHUNG, 3000, &dwResult);
999 
1000     /* Free the buffer */
1001     RtlFreeHeap(RtlGetProcessHeap(), 0, pdata);
1002 
1003     return (ret && dwResult) ? TRUE : FALSE;
1004 }
1005 
1006 static
1007 HARDERROR_RESPONSE
1008 UserpMessageBox(
1009     IN PUNICODE_STRING TextStringU,
1010     IN PUNICODE_STRING CaptionStringU,
1011     IN UINT  Type,
1012     IN ULONG Timeout)
1013 {
1014     ULONG MessageBoxResponse;
1015     HDESK hDesk, hOldDesk;
1016 
1017     DPRINT("Text = '%S', Caption = '%S', Type = 0x%lx\n",
1018            TextStringU->Buffer, CaptionStringU->Buffer, Type);
1019 
1020     // TEMPORARY HACK to fix desktop assignment for harderror message boxes.
1021     hDesk = OpenInputDesktop(0, FALSE, GENERIC_WRITE);
1022     if (!hDesk)
1023         return ResponseNotHandled;
1024 
1025     /* Assign the desktop to this thread */
1026     hOldDesk = GetThreadDesktop(GetCurrentThreadId());
1027     if (!SetThreadDesktop(hDesk))
1028     {
1029         CloseDesktop(hDesk);
1030         return ResponseNotHandled;
1031     }
1032 
1033     /* Display a message box */
1034     MessageBoxResponse = MessageBoxTimeoutW(NULL,
1035                                             TextStringU->Buffer,
1036                                             CaptionStringU->Buffer,
1037                                             Type, 0, Timeout);
1038 
1039     /* Restore the original desktop */
1040     SetThreadDesktop(hOldDesk);
1041     CloseDesktop(hDesk);
1042 
1043     /* Return response value */
1044     switch (MessageBoxResponse)
1045     {
1046         case IDOK:       return ResponseOk;
1047         case IDCANCEL:   return ResponseCancel;
1048         case IDYES:      return ResponseYes;
1049         case IDNO:       return ResponseNo;
1050         case IDABORT:    return ResponseAbort;
1051         case IDIGNORE:   return ResponseIgnore;
1052         case IDRETRY:    return ResponseRetry;
1053         case IDTRYAGAIN: return ResponseTryAgain;
1054         case IDCONTINUE: return ResponseContinue;
1055         default:         return ResponseNotHandled;
1056     }
1057 
1058     return ResponseNotHandled;
1059 }
1060 
1061 static
1062 VOID
1063 UserpLogHardError(
1064     IN PUNICODE_STRING TextStringU,
1065     IN PUNICODE_STRING CaptionStringU)
1066 {
1067     NTSTATUS Status;
1068     HANDLE hEventLog;
1069     UNICODE_STRING UNCServerNameU = {0, 0, NULL};
1070     UNICODE_STRING SourceNameU = RTL_CONSTANT_STRING(L"Application Popup");
1071     PUNICODE_STRING Strings[] = {CaptionStringU, TextStringU};
1072 
1073     Status = ElfRegisterEventSourceW(&UNCServerNameU, &SourceNameU, &hEventLog);
1074     if (!NT_SUCCESS(Status) || !hEventLog)
1075     {
1076         DPRINT1("ElfRegisterEventSourceW failed with Status 0x%08lx\n", Status);
1077         return;
1078     }
1079 
1080     Status = ElfReportEventW(hEventLog,
1081                              EVENTLOG_INFORMATION_TYPE,
1082                              0,
1083                              STATUS_LOG_HARD_ERROR,
1084                              NULL,      // lpUserSid
1085                              ARRAYSIZE(Strings),
1086                              0,         // dwDataSize
1087                              Strings,
1088                              NULL,      // lpRawData
1089                              0,
1090                              NULL,
1091                              NULL);
1092     if (!NT_SUCCESS(Status))
1093         DPRINT1("ElfReportEventW failed with Status 0x%08lx\n", Status);
1094 
1095     ElfDeregisterEventSource(hEventLog);
1096 }
1097 
1098 VOID
1099 NTAPI
1100 UserServerHardError(
1101     IN PCSR_THREAD ThreadData,
1102     IN PHARDERROR_MSG Message)
1103 {
1104     ULONG ErrorMode;
1105     UINT  dwType = 0;
1106     ULONG Timeout = INFINITE;
1107     UNICODE_STRING TextU, CaptionU;
1108     WCHAR LocalTextBuffer[256];
1109     WCHAR LocalCaptionBuffer[256];
1110 
1111     ASSERT(ThreadData->Process != NULL);
1112 
1113     /* Default to not handled */
1114     Message->Response = ResponseNotHandled;
1115 
1116     /* Make sure we don't have too many parameters */
1117     if (Message->NumberOfParameters > MAXIMUM_HARDERROR_PARAMETERS)
1118     {
1119         // NOTE: Windows just fails (STATUS_INVALID_PARAMETER) & returns ResponseNotHandled.
1120         DPRINT1("Invalid NumberOfParameters = %d\n", Message->NumberOfParameters);
1121         Message->NumberOfParameters = MAXIMUM_HARDERROR_PARAMETERS;
1122     }
1123     if (Message->ValidResponseOptions > OptionCancelTryContinue)
1124     {
1125         DPRINT1("Unknown ValidResponseOptions = %d\n", Message->ValidResponseOptions);
1126         return; // STATUS_INVALID_PARAMETER;
1127     }
1128     if (Message->Status == STATUS_SERVICE_NOTIFICATION)
1129     {
1130         if (Message->NumberOfParameters < 3)
1131         {
1132             DPRINT1("Invalid NumberOfParameters = %d for STATUS_SERVICE_NOTIFICATION\n",
1133                     Message->NumberOfParameters);
1134             return; // STATUS_INVALID_PARAMETER;
1135         }
1136         // (Message->UnicodeStringParameterMask & 0x3)
1137     }
1138 
1139     /* Re-initialize the hard errors cache */
1140     UserInitHardErrorsCache();
1141 
1142     /* Format the message caption and text */
1143     RtlInitEmptyUnicodeString(&TextU, LocalTextBuffer, sizeof(LocalTextBuffer));
1144     RtlInitEmptyUnicodeString(&CaptionU, LocalCaptionBuffer, sizeof(LocalCaptionBuffer));
1145     UserpFormatMessages(&TextU, &CaptionU, &dwType, &Timeout, Message);
1146 
1147     /* Log the hard error message */
1148     UserpLogHardError(&TextU, &CaptionU);
1149 
1150     /* Display a hard error popup depending on the current ErrorMode */
1151 
1152     /* Query the error mode value */
1153     ErrorMode = GetRegInt(L"\\Registry\\Machine\\System\\CurrentControlSet\\Control\\Windows",
1154                           L"ErrorMode", 0);
1155 
1156     if (Message->Status != STATUS_SERVICE_NOTIFICATION && ErrorMode != 0)
1157     {
1158         /* Returns OK for the hard error */
1159         Message->Response = ResponseOk;
1160         goto Quit;
1161     }
1162 
1163     if (Message->ValidResponseOptions == OptionOkNoWait)
1164     {
1165         /* Display the balloon */
1166         Message->Response = ResponseOk;
1167         if (UserpShowInformationBalloon(&TextU,
1168                                         &CaptionU,
1169                                         dwType,
1170                                         Message))
1171         {
1172             Message->Response = ResponseOk;
1173             goto Quit;
1174         }
1175     }
1176 
1177     /* Display the message box */
1178     Message->Response = UserpMessageBox(&TextU,
1179                                         &CaptionU,
1180                                         dwType,
1181                                         Timeout);
1182 
1183 Quit:
1184     /* Free the strings if they have been reallocated */
1185     if (TextU.Buffer != LocalTextBuffer)
1186         RtlFreeUnicodeString(&TextU);
1187     if (CaptionU.Buffer != LocalCaptionBuffer)
1188         RtlFreeUnicodeString(&CaptionU);
1189 
1190     return;
1191 }
1192 
1193 VOID
1194 UserInitHardErrorsCache(VOID)
1195 {
1196     NTSTATUS Status;
1197     LCID CurrentUserLCID = 0;
1198 
1199     Status = NtQueryDefaultLocale(TRUE, &CurrentUserLCID);
1200     if (!NT_SUCCESS(Status) || CurrentUserLCID == 0)
1201     {
1202         /* Fall back to english locale */
1203         DPRINT1("NtQueryDefaultLocale failed with Status = 0x%08lx\n", Status);
1204         // LOCALE_SYSTEM_DEFAULT;
1205         CurrentUserLCID = MAKELCID(MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US), SORT_DEFAULT);
1206     }
1207     if (g_CurrentUserLangId == LANGIDFROMLCID(CurrentUserLCID))
1208     {
1209         /* The current lang ID and the hard error strings have already been cached */
1210         return;
1211     }
1212 
1213     /* Load the strings using the current system locale */
1214     RtlLoadUnicodeString(UserServerDllInstance, IDS_SEVERITY_SUCCESS,
1215                          &g_SuccessU, L"Success");
1216     RtlLoadUnicodeString(UserServerDllInstance, IDS_SEVERITY_INFORMATIONAL,
1217                          &g_InformationU, L"System Information");
1218     RtlLoadUnicodeString(UserServerDllInstance, IDS_SEVERITY_WARNING,
1219                          &g_WarningU, L"System Warning");
1220     RtlLoadUnicodeString(UserServerDllInstance, IDS_SEVERITY_ERROR,
1221                          &g_ErrorU, L"System Error");
1222     // "unknown software exception"
1223     RtlLoadUnicodeString(UserServerDllInstance, IDS_SYSTEM_PROCESS,
1224                          &g_SystemProcessU, L"System Process");
1225     RtlLoadUnicodeString(UserServerDllInstance, IDS_OK_TERMINATE_PROGRAM,
1226                          &g_OKTerminateU, L"Click on OK to terminate the program.");
1227     RtlLoadUnicodeString(UserServerDllInstance, IDS_CANCEL_DEBUG_PROGRAM,
1228                          &g_CancelDebugU, L"Click on CANCEL to debug the program.");
1229 
1230     /* Remember that we cached the hard error strings */
1231     g_CurrentUserLangId = LANGIDFROMLCID(CurrentUserLCID);
1232 }
1233 
1234 /* EOF */
1235