CORE-20279
PRELIMINARY REMARK: The described bug and code workaround only applies
for x86 32-bit builds.
----
While the Winlogon notification handlers[^1] actually use a `STDCALL`
calling convention, which can be trivially verified by debugging the
official Windows <= 2003 winlogon.exe and its notification extensions,
there exist 3rd-party Winlogon notification DLLs, like the `Ati2evxx.dll`
one from AMD/ATI XP video drivers, that use a `CDECL` calling convention,
or an invalid number (zero) of parameters.
I think the reason why this happens is as follows.
The official documentation[^1] indicates that the handlers have the
following prototype:
```c
void Event_Handler_Function_Name(
_In_ PWLX_NOTIFICATION_INFO pInfo
);
```
The documentation (and possibly the internal header Windows is using for
Winlogon) is sloppy, because it doesn't tell whether the convention is
`STDCALL` or `CDECL`. When compiling routines with such a signature, the
compiler will employ whatever default convention it is set to use.
Windows code is typically compiled with `STDCALL` convention as the default
(see e.g. how the Windows Development Kit is set up), thus, such a
function signature would default to `STDCALL`. Observation (with debugger)
shows that it is what Windows' winlogon.exe is indeed expecting.
However, 3rd-party code using a different development environment, could
set the compiler to use `CDECL` as the default calling convention. As a
result, the function signature from above would use `CDECL` instead.
The difference between the `STDCALL` and `CDECL` conventions is how the
function parameters are passed on the stack and how the stack is cleaned
at the end (`STDCALL`: the function unwinds the stack; `CDECL`: the caller
does it). A calling convention mismatch would therefore corrupt the stack,
and this is exactly what happens with the `Ati2evxx.dll` from the AMD/ATI
drivers, see CORE-20279.
The ReactOS Winlogon crashes from the `_RTC_Failure()` handler just after
the 3rd-party handler returns, since we compile our code with runtime checks
enabled. Windows' winlogon.exe doesn't apparently crash, because neither
in Release nor in Checked/Debug mode did they compile winlogon.exe with
RTC enabled. However, its stack would become more corrupt with time.
In order to alleviate this in ReactOS' winlogon.exe, I decided to use
a "generic" workaround, manually calling the handler with inline ASM
(which is OK since the problem and solution is x86-specific only).
It does something similar to what the RTC support does: it checks the
stack pointer after the call and restores it if needed.
An informative message is then emitted in the debugger telling which DLL
is buggy and needs to be fixed.
[^1]: https://learn.microsoft.com/en-us/windows/win32/secauthn/event-handler-function-prototype
Test results:
- Test 1a: `Asynchronous: FALSE, Impersonation: FALSE`
BEFORE the fix:
OK, the thread isn't impersonated:
```
Thread Token : 0x00000000 - User: '(null)\(null)'
```
BUT these two WLX notify info fields aren't set when the user is logged in:
```
Info.UserName : '(null)'
Info.Domain : '(null)'
```
Results:
```
WLEventStartup: 30 tests executed (0 marked as todo, 1 failure), 0 skipped.
WLEventLogon: 30 tests executed (0 marked as todo, 3 failures), 2 skipped.
WLEventStartShell: 30 tests executed (0 marked as todo, 3 failures), 2 skipped.
(NOTE: WLEventPostShell isn't yet implemented in ReactOS)
WLEventLock: 30 tests executed (0 marked as todo, 4 failures), 2 skipped.
WLEventUnlock: 30 tests executed (0 marked as todo, 3 failures), 2 skipped.
WLEventStartScreenSaver: 30 tests executed (0 marked as todo, 9 failures), 2 skipped.
WLEventStopScreenSaver: 30 tests executed (0 marked as todo, 7 failures), 2 skipped.
WLEventLogoff: 30 tests executed (0 marked as todo, 4 failures), 2 skipped.
WLEventShutdown: 31 tests executed (0 marked as todo, 3 failures), 0 skipped.
```
- Test 1b: `Asynchronous: FALSE, Impersonation: FALSE`
AFTER the fix:
OK, the thread isn't impersonated:
```
Thread Token : 0x00000000 - User: '(null)\(null)'
```
OK, these two WLX notify info fields are now set when the user is logged in:
```
Info.UserName : 'Administrator'
Info.Domain : 'MYCOMPUTERNAME'
```
Results:
```
WLEventStartup: 30 tests executed (0 marked as todo, 1 failure), 0 skipped.
WLEventLogon: 32 tests executed (0 marked as todo, 1 failure), 0 skipped.
WLEventStartShell: 32 tests executed (0 marked as todo, 1 failure), 0 skipped.
(NOTE: WLEventPostShell isn't yet implemented in ReactOS)
WLEventLock: 32 tests executed (0 marked as todo, 2 failures), 0 skipped.
WLEventUnlock: 32 tests executed (0 marked as todo, 1 failure), 0 skipped.
WLEventStartScreenSaver: 32 tests executed (0 marked as todo, 7 failures), 0 skipped.
WLEventStopScreenSaver: 32 tests executed (0 marked as todo, 5 failures), 0 skipped.
WLEventLogoff: 32 tests executed (0 marked as todo, 2 failures), 0 skipped.
WLEventShutdown: 31 tests executed (0 marked as todo, 3 failures), 0 skipped.
```
Less failed tests and none skipped anymore.
- Test 2a: `Asynchronous: FALSE, Impersonation: TRUE`
BEFORE the fix:
OK, the thread impersonates the user when (s)he is logged in:
```
Thread Token : 0x00000360 - User: 'MYCOMPUTERNAME\Administrator'
```
BUT these two WLX notify info fields aren't set when the user is logged in:
```
Info.UserName : '(null)'
Info.Domain : '(null)'
```
Results:
```
WLEventStartup: 30 tests executed (0 marked as todo, 1 failure), 0 skipped.
WLEventLogon: 31 tests executed (0 marked as todo, 5 failures), 2 skipped.
WLEventStartShell: 31 tests executed (0 marked as todo, 5 failures), 2 skipped.
(NOTE: WLEventPostShell isn't yet implemented in ReactOS)
WLEventLock: 31 tests executed (0 marked as todo, 6 failures), 2 skipped.
WLEventUnlock: 31 tests executed (0 marked as todo, 5 failures), 2 skipped.
WLEventStartScreenSaver: 32 tests executed (0 marked as todo, 6 failures), 2 skipped.
WLEventStopScreenSaver: 32 tests executed (0 marked as todo, 4 failures), 2 skipped.
WLEventLogoff: 32 tests executed (0 marked as todo, 4 failures), 2 skipped.
WLEventShutdown: 31 tests executed (0 marked as todo, 3 failures), 0 skipped.
```
- Test 2b: `Asynchronous: FALSE, Impersonation: TRUE`
AFTER the fix:
OK, the thread impersonates the user when (s)he is logged in:
```
Thread Token : 0x00000360 - User: 'MYCOMPUTERNAME\Administrator'
```
OK, these two WLX notify info fields are now set when the user is logged in:
```
Info.UserName : 'Administrator'
Info.Domain : 'MYCOMPUTERNAME'
```
Results:
```
WLEventStartup: 30 tests executed (0 marked as todo, 1 failure), 0 skipped.
WLEventLogon: 33 tests executed (0 marked as todo, 3 failures), 0 skipped.
WLEventStartShell: 33 tests executed (0 marked as todo, 3 failures), 0 skipped.
(NOTE: WLEventPostShell isn't yet implemented in ReactOS)
WLEventLock: 33 tests executed (0 marked as todo, 4 failures), 0 skipped.
WLEventUnlock: 33 tests executed (0 marked as todo, 3 failures), 0 skipped.
WLEventStartScreenSaver: 34 tests executed (0 marked as todo, 4 failures), 0 skipped.
WLEventStopScreenSaver: 34 tests executed (0 marked as todo, 2 failures), 0 skipped.
WLEventLogoff: 34 tests executed (0 marked as todo, 2 failures), 0 skipped.
WLEventShutdown: 31 tests executed (0 marked as todo, 3 failures), 0 skipped.
```
As well, less failed tests and none skipped anymore.
Also, capture in `CallNotificationDll()` the notify info structure template
before invoking each DLL handler: a malicious caller might modify its contents,
and so we need to re-initialize its contents for each call.
Test results:
- Test 1: `Asynchronous: FALSE, Impersonation: FALSE`
Before & After the fix, give the same results:
```
WLEventStartup: 30 tests executed (0 marked as todo, 1 failure), 0 skipped.
WLEventLogon: 30 tests executed (0 marked as todo, 3 failures), 2 skipped.
WLEventStartShell: 30 tests executed (0 marked as todo, 3 failures), 2 skipped.
(NOTE: WLEventPostShell isn't yet implemented in ReactOS)
WLEventLock: 30 tests executed (0 marked as todo, 4 failures), 2 skipped.
WLEventUnlock: 30 tests executed (0 marked as todo, 3 failures), 2 skipped.
WLEventStartScreenSaver: 30 tests executed (0 marked as todo, 9 failures), 2 skipped.
WLEventStopScreenSaver: 30 tests executed (0 marked as todo, 7 failures), 2 skipped.
WLEventLogoff: 30 tests executed (0 marked as todo, 4 failures), 2 skipped.
WLEventShutdown: 31 tests executed (0 marked as todo, 3 failures), 0 skipped.
```
* The current notification thread doesn't impersonate any user, the
dumped thread token shows:
`Thread Token : 0x00000000 - User: '(null)\(null)'`
(except for `WLEventStartScreenSaver` and `WLEventStopScreenSaver`
which currently in ReactOS, impersonate the logged user and shows e.g.,
`Thread Token : 0x00000250 - User: 'MYCOMPUTERNAME\Administrator' ).
- Test 2: `Asynchronous: FALSE, Impersonation: TRUE`
Before the fix:
```
WLEventStartup: 30 tests executed (0 marked as todo, 1 failure), 0 skipped.
WLEventLogon: 29 tests executed (0 marked as todo, 8 failures), 4 skipped.
WLEventStartShell: 29 tests executed (0 marked as todo, 8 failures), 4 skipped.
(NOTE: WLEventPostShell isn't yet implemented in ReactOS)
WLEventLock: 29 tests executed (0 marked as todo, 9 failures), 4 skipped.
WLEventUnlock: 29 tests executed (0 marked as todo, 8 failures), 4 skipped.
WLEventStartScreenSaver: 32 tests executed (0 marked as todo, 6 failures), 2 skipped.
WLEventStopScreenSaver: 32 tests executed (0 marked as todo, 4 failures), 2 skipped.
WLEventLogoff: 30 tests executed (0 marked as todo, 7 failures), 4 skipped.
WLEventShutdown: 31 tests executed (0 marked as todo, 3 failures), 0 skipped.
```
* There are more failures (and skipped tests), because in impersonation
mode, the tests expect that the current thread is impersonating the
logged-on user, and validate whether this impersonated user matches
what the WLX notification `Domain` and `UserName` information structure
members are. However, these two fields aren't set yet, and the thread
isn't impersonating any user yet.
* The dumped thread token shows the same information as in Test 1.
- Test 3: `Asynchronous: FALSE, Impersonation: TRUE`
After the fix:
```
WLEventStartup: 30 tests executed (0 marked as todo, 1 failure), 0 skipped.
WLEventLogon: 31 tests executed (0 marked as todo, 5 failures), 2 skipped.
WLEventStartShell: 31 tests executed (0 marked as todo, 5 failures), 2 skipped.
(NOTE: WLEventPostShell isn't yet implemented in ReactOS)
WLEventLock: 31 tests executed (0 marked as todo, 6 failures), 2 skipped.
WLEventUnlock: 31 tests executed (0 marked as todo, 5 failures), 2 skipped.
WLEventStartScreenSaver: 32 tests executed (0 marked as todo, 6 failures), 2 skipped.
WLEventStopScreenSaver: 32 tests executed (0 marked as todo, 4 failures), 2 skipped.
WLEventLogoff: 32 tests executed (0 marked as todo, 4 failures), 2 skipped.
WLEventShutdown: 31 tests executed (0 marked as todo, 3 failures), 0 skipped.
```
* There are now less skipped tests than in Test 2, as much as in Test 1.
* There are also less failed tests than in Test 2; however, there are
few more failed tests than in Test 1, but this is balanced by new
tests being executed.
The reason is that now, compared to Test 2 in impersonation mode, the
current thread is correctly impersonating the logged-on user, if any.
However, the WLX notification `Domain` and `UserName` information
structure members still aren't set yet, thus some of the tests fail.
* The dumped thread token for all notifications now show e.g.,
`Thread Token : 0x00000360 - User: 'MYCOMPUTERNAME\Administrator'`
(`WLEventStartup`, `WLEventShutdown` don't show any, since no user
is logged-in at these stages.)
And retrieve all its exported notification handlers.
This reverts parts of commit 5d4f69bf35.
Because notification DLLs can keep internal state between successive
notifications, they are kept loaded until Winlogon termination.
The SFC notification DLL is also loaded. Because sfc.dll redirects to
sfc_os.dll, the latter is then automatically loaded. This allows doing
what commit 88ee639b06 (r68615) was aiming at, in a less hackish manner.
CORE-9598
Test results:
- Before:
We observe the repeated loading/unloading of the DLL at each
notification call:
```
WLNOTIFY(ac.b0): Entering `DllMain`(hInst: 0x10000000, dwReason: 0x1, pReserved: 0x00000000)
...
WLNOTIFY(ac.b0): Entering `DllMain`(hInst: 0x10000000, dwReason: 0x0, pReserved: 0x00000000)
```
The global state of the notification DLL is lost between each
notification, and such testing errors happen:
```
modules\rostests\win32\winlogon\wlntfytests\wlntfytests.c:786: Test failed: **** WLEventLogon: ERROR: Wrong state NON-INITIALIZED, expected Startup or Logoff
err:(modules\rostests\win32\winlogon\wlntfytests\wlntfytests.c:1034) **** WLEventLogon: Changing state NON-INITIALIZED to Logon
```
The previous notification state is always reset to `NON-INITIALIZED` (its initial value).
Test results for all the notifications:
```
WLEventStartup: 30 tests executed (0 marked as todo, 1 failure), 0 skipped.
WLEventLogon: 30 tests executed (0 marked as todo, 4 failures), 2 skipped.
WLEventStartShell: 30 tests executed (0 marked as todo, 4 failures), 2 skipped.
(NOTE: WLEventPostShell isn't yet implemented in ReactOS)
WLEventLock: 30 tests executed (0 marked as todo, 4 failures), 2 skipped.
WLEventUnlock: 30 tests executed (0 marked as todo, 4 failures), 2 skipped.
WLEventStartScreenSaver: 30 tests executed (0 marked as todo, 10 failures), 0 skipped.
WLEventStopScreenSaver: 30 tests executed (0 marked as todo, 9 failures), 0 skipped.
WLEventLogoff: 30 tests executed (0 marked as todo, 5 failures), 2 skipped.
WLEventShutdown: 31 tests executed (0 marked as todo, 5 failures), 0 skipped.
```
- After:
The DLL is loaded only once, at the first notification, then stays
loaded for the whole life of winlogon.exe:
```
WLNOTIFY(ac.b0): Entering `DllMain`(hInst: 0x10000000, dwReason: 0x1, pReserved: 0x00000000)
```
and the global state of the notification DLL is now kept between each
notification, there are no `ERROR: Wrong state NON-INITIALIZED, expected ...`
errors anymore.
Test results for all the notifications:
```
WLEventStartup: 30 tests executed (0 marked as todo, 1 failure), 0 skipped.
WLEventLogon: 30 tests executed (0 marked as todo, 3 failures), 2 skipped.
WLEventStartShell: 30 tests executed (0 marked as todo, 3 failures), 2 skipped.
(NOTE: WLEventPostShell isn't yet implemented in ReactOS)
WLEventLock: 30 tests executed (0 marked as todo, 4 failures), 2 skipped.
WLEventUnlock: 30 tests executed (0 marked as todo, 3 failures), 2 skipped.
WLEventStartScreenSaver: 30 tests executed (0 marked as todo, 9 failures), 2 skipped.
WLEventStopScreenSaver: 30 tests executed (0 marked as todo, 7 failures), 2 skipped.
WLEventLogoff: 30 tests executed (0 marked as todo, 4 failures), 2 skipped.
WLEventShutdown: 31 tests executed (0 marked as todo, 3 failures), 0 skipped.
```
Expand the "DllName" notification DLL path string, independently of
whether it is REG_SZ or REG_EXPAND_SZ, before loading it, similarly
to Windows' winlogon.exe.
Validate also the type/size of the retrieved registry value.
Historical note:
Investigation shows that this functionality, introduced between builds
1902 and 1906 of Windows NT 5.0 (future 2000) Beta 3, has always been
"nop-ed" and has remained this way till Windows Server 2003.
The value read from the "SafeMode" registry value is unconditionally
overridden afterwards, causing the notification DLLs to always be loaded.
In ReactOS, this functionality is restored, and only the notifications
tagged as such are loaded in SafeMode.
Furthermore:
Analysis of strings in Win2000 and WinXP/2003 winlogon.exe, show that
the "Safe" registry value doesn't exist for notifications; instead,
it is named "SafeMode".
The "Safe" value appears only for the SensLogn (SENS Winlogon Event) handler
registry entry. My hypothesis is that the value name is a typo for the
"SafeMode" value. It has been introduced in the `\Winlogon\Notify\SensLogn`
registry entry for SensLogn around Windows NT 5.0 build 1946.
Analysis of strings in Win2000 and WinXP/2003 winlogon.exe, show that
the "Enabled" registry value doesn't exist for notifications.
This value is actually only used by the ScCertProp (Smart Card Certificate
Propagation) notifications, in wlnotify.dll, to enable or disable certificate
progagation.[^1]
Note that whatever the "Enabled" registry value is, the notification DLL
is still loaded within winlogon.exe.
We however keep the `bEnabled` internal flag, so as to be able to disable
at runtime notifications that could not be delay-loaded, or that behave
erratically, etc.
[WLNOTIFY] Add a comment about the "Enabled" value in scard.c!SCardStartCertProp()
[^1]: For more information, see:
https://www.microfocus.com/documentation/securelogin/9.0/administration_guide/blm54qb.html?view=printhttps://www.betaarchive.com/wiki/index.php?title=Microsoft_KB_Archive/925884
- Add DeleteNotification() helper;
- Invoke it in failure path in AddNotificationDll(), and in
CleanupNotifications().
- Simplify iteration loops in CallNotificationDlls() and in
CleanupNotifications().
Side-note: When doing `Notification = CONTAINING_RECORD(ListEntry, ...);`
where `ListEntry` iterates over a well-formed linked-list, the `Notification`
pointer can **NEVER** be NULL!
This includes:
- Notification dll calling in CallNotificationDll().
- winmm.dll API calling (e.g. PlaySound) in PlaySoundRoutine().
Also:
- Fix dwKeyName usage in RegEnumKeyExW() specifying a number of *characters*.