May 20th, 2020

The case of the SHGet­Folder­Path(CSIDL_COMMON_DOCUMENTS) that returned ERROR_PATH_NOT_FOUND

A customer was experiencing a problem with the SHGet­Folder­Path function. Specifically, they had a program that called the function like this:

SHGetFolderPath(NULL, CSIDL_COMMON_DOCUMENTS, NULL,
                SHGFP_TYPE_CURRENT, pathBuffer);

but it failed with error 0x80070003 which is the HRESULT version of ERROR_PATH_NOT_FOUND. The error occurs only when run from a Jenkins pipeline. If they the run the program standalone, then the function succeeds and returns the expected result.

A procmon trace showed that the application tried to access the folder C:\Windows\SysWOW64\autobuild\Documents, which failed with NAME_NOT_FOUND. And that was the clue that broke things open.

The Common Documents folder defaults to %PUBLIC%\Documents. The PUBLIC environment variable’s normal value is C:\Users\Public, but when the program runs as part of a Jenkins pipeline, the environment variable is set to autobuild for some reason.

This means that when the program calls SHGet­Folder­Path and asks for CSIDL_COMMON_DOCUMENTS, the system looks for autobuild\Documents, which doesn’t exist, hence error 0x80070003: “The system cannot find the path specified.”

There are a number of environment variables that have special meaning, and you change them at your peril. You probably know about variables like windir, ProgramFiles, and TEMP, but there are quite a number of other special environment variables, and PUBLIC is one of them.

Armed with this information, the customer went back to see who was messing with the PUBLIC environment variable and try to get them to stop.

Topics
Code

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

25 comments

Discussion is closed. Login to edit/delete existing comments.

  • Stefan Kanthak

    “Accidentally reconfiguring the system.”
    Its time to learn a new german word: Unfallverhütungsvorschrift!
    Also “Software Engineering” may come into your mind…
    A PROPERLY designed and implemented product does NOT allow such accidents!

  • skSdnW

    %ProgramData% relative special folders have the same issue.

    The strange thing is, this expansion issue only applies to the older special folders. Even if you use the newer Known Folder API, the old special folders have this issue (Common Templates etc.) while the KF-only folders (CommonRingtones etc.) do not.

  • Matthew Jackson

    I recently came across something certainly related to this. I was surprised that scripts running under Jenkins jobs were accessing user profile information from C:\Windows\ instead of C:\Users\. I chalked it up to the fact that we were running Jenkins as a Windows Service.

    • Stefan Kanthak

      Normal behaviour if the profile used is C:\Windows\System32\Config\SystemProfile, C:\Windows\ServiceProfiles\LocalService or C:\Windows\ServiceProfiles\NetworkService
      The latter two are the profiles for the "NT AUTHORITY\LOCAL SERVICE" alias "LocalService" and the "NT AUTHORITY\NETWORK SERVICE" alias "NetworkService" accounts, both introduced with Windows XP, initially stored in the directories "C:\Documents and Settings\LocalService" and "C:\Documents and Settings\NetworkService", relocated to their current locations with Vista.
      The first profile, introduced with Windows 2000, is for the "NT AUTHORITY\SYSTEM" alias "LocalSystem" account.
      Its location is yet...

      Read more
  • Ondra HoÅ¡ek

    Is there a canonical list of environment variables that have special meaning?

  • Stefan Kanthak

    Thanks for posting about one of Windows many design bugs.
    This one was introduced with Vista.
    Before Vista, the "All Users" profile was used to store "Common Desktop", "Common Documents" etc.
    The base path to this profile is provided by the function GetAllUsersProfileDirectory() (https://msdn.microsoft.com/en-us/library/bb762276.aspx), from which the environment variable ALLUSERSPROFILE is set during user login.
    The Directory Shuffle performed for Vista introduced the "Public" profile and C:\ProgramData, accompanied by the new environment variables PUBLIC and...

    Read more
    • Danielix Klimax

      Looks like you are just exploiting yourself. What is gained by attacker that wouldn’t be available otherwise. Big talk and no evidence so far (applies to your previous “vulnerability” assertions too)

      • Raymond ChenMicrosoft employee Author

        So far as I can tell, the only person whose service was denied is yourself. It is not a security vulnerability that you can make your own life miserable.

      • Danielix Klimax

        @Stefan Kanthak

        Still throwing stuff of strongly dubious relevance to this discussion. And making even more dubious but strongly worded assertions won’t improve your position either.
        Since you are throwing big words with little understanding then you may want to make yourself familiar with “The other side of airlock” and “absence of attacker’s gain”.

      • Stefan Kanthak

        You ignored but CWE-426, CWE-427 and CAPEC-471
        Not only that property of the card house known as Windows made and makes life of millions of your customers miserable.
        Ever heard somebody whisper DEFENSE IN DEPTH?

  • Vadim Zeitlin

    How topical, I just wish I had read this yesterday, before spending more than an hour trying to find why CryptAcquireContext() failed with a mysterious NTE_PROVIDER_DLL_FAIL error when launching the program using remote debugger, only to find that it relies on SystemRoot being set in the environment, which wasn't the case.

    I still wonder why do the API functions like this use the environment variables instead of using the registry, isn't this what it is for?...

    Read more
    • Raymond ChenMicrosoft employee Author

      I suspect that the API is getting the provider from the registry. What’s happening is that the provider is listed in the registry with a REG_EXPAND_SZ %SystemRoot%. (There is no REG_INSERT_WINDOWS_DIRECTORY_SZ registry value type.)

      • Stefan Kanthak

        "I suspect that the API is getting the provider from the registry."

        And said "provider" is most likely located in the "system directory", so every sane developer would have changed their implementation to use LoadLibraryEx(L"‹provider›.dll", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32) instead of fiddling with either %SystemRoot% or GetWindowsDirectory()/GetSystemDirectory()
        JFTR: LOAD_LIBRARY_SEARCH_SYSTEM32 is available since Windows 8 out-of-the-box, and with update KB2533623 also available for Windows Vista and 7

        Read more
      • Raymond ChenMicrosoft employee Author

        It would be presumptuous of the consumer to assume that “Well, anybody who writes a provider is going to put it in the system directory.”

      • Stefan Kanthak

        OUCH¹: please ask your colleagues who implemented LoadLibraryEx() how it works when called with both a fully qualified path and the LOAD_LIBRARY_SEARCH_SYSTEM32 flag.
        OUCH²: please ask your colleagues who implemented the consumer why they load DLLs from arbitrary user-controlled paths.
        OUCH³: please ask your colleagues who implemented the installer of the provider whether they intended to add another weakness to Windows!
        And don't forget to ask your colleagues who published the documentation of CryptAcquireContext()...

        Read more
      • Stefan Kanthak

        "(There is no REG_INSERT_WINDOWS_DIRECTORY_SZ registry value type)"

        OUCH: of course there is!
        *.MSI, processed by the Windows Installer, have symbolic names for that.
        *.INF, interpreted by the SetupAPI, provide the LDID %10% as well as the DIRID %16420% to resolve %SystemRoot% during installation!
        See https://msdn.microsoft.com/en-us/library/ff553598.aspx and https://msdn.microsoft.com/en-us/library/ff560821.aspx for not just these two LDIDs/DIRIDs
        For a demonstration, fetch the scripts https://skanthak.homepage.t-online.de/download/LDID.INF and https://skanthak.homepage.t-online.de/download/DIRID.INF and "install" them. See https://skanthak.homepage.t-online.de/gimmick.html#dirid for a rather short description.

        Read more
      • Raymond ChenMicrosoft employee Author

        You’re not contradicting me. There is no REG_INSERT_WINDOWS_DIRECTORY_SZ registry value type. What you provided was ways of substituting the Windows directory before the string even gets added to the registry. But those aren’t of any help for OS components, because OS components aren’t installed via MSI or INF files.

      • Stefan Kanthak

        I doubt that the installer for OS components misses an equivalent (and if it does, that's yet another design bug): the majority of paths in the registry shipped with current versions of Windows are fix/constant, i.e. they start with C:\
        Since Windows XP Embedded, from which Vista's Component Based Servicing was largely inspired, SETUP.EXE allows to assign an arbitrary drive letter for the "system drive", and "fixes" all pathnames beginning with C:\ not just in...

        Read more
    • Stefan Kanthak

      There’s absolutely no need to query the registry: GetWindowsDirectory() and GetSystemDirectory() exist!

  • Chris

    Getting build environments to play nice with Jenkins is all part of the fun.

    • Ian Kemp

      You mean, getting Jenkins to play nice…

  • GL

    I expected SHGetFolderPath to dig into a hive under HKLM 🤔

    • anonymous

      This comment has been deleted.

    • Stefan Kanthak

      There?
      <code>

      Better read these entries beforehand:
      <code>

      Read more