August 16th, 2021

What are these dire multithreading consequences that the GetFullPathName documentation is trying to warn me about?

The documentation for the Get­Full­Path­Name function contains this dire warning:

Multithreaded applications and shared library code should not use the GetFullPathName function and should avoid using relative path names. The current directory state written by the SetCurrentDirectory function is stored as a global variable in each process, therefore multithreaded applications cannot reliably use this value without possible data corruption from other threads that may also be reading or setting this value. This limitation also applies to the SetCurrentDirectory and GetCurrentDirectory functions. The exception being when the application is guaranteed to be running in a single thread, for example parsing file names from the command line argument string in the main thread prior to creating any additional threads. Using relative path names in multithreaded applications or shared library code can yield unpredictable results and is not supported.

What is this warning trying to say? It seems to suggest that the current directory global variable is not thread-safe. Does this mean that all calls to Set­Current­Directory and Get­Current­Directory need to be serialized by the application?

No, that’s not what it’s saying.

What it’s trying to say is that the meaning of a relative path depends on the current value of the current directory. The value of the current directory can be changed by any thread at any time, so make sure that you understand that the result of the Get­Full­Path­Name function is a “moment in time” calculation. Resolving the same relative path in consecutive calls to the Get­Full­Path­Name function could result in different results if the current directory changed in between.

If you find yourself with a relative path, you have a few choices.

One option is to pass it as a relative path to a function like Create­File, but only once, and as soon as possible. Don’t assume that a second Create­File will open the same file. You want to use it as soon as possible because the user entered the relative path based on some underlying assumptions about the current directory, and the longer you wait, the more likely those assumptions are going to be wrong.

Another option is to convert it to an absolute path as soon as possible, and use the absolute path (at your leisure) from then on. Again, you want to convert it as soon as possible to reduce the likelihood of changes to the conditions under which the user entered the relative path.

If the relative path didn’t come from the user but rather from, say, a configuration file or another process, then things are kind of sketchy. The relative path in the configuration file is probably intended to be interpreted relative to some anchor point (such as the configuration file itself), not relative to something as fickle as the process’s current directory. And a relative path received from another process is even more sketchy, because that other process has no idea what your current directory is.

The concept of the current directory was inherited from MS-DOS, which in turn got it from the concept of the current drive in CP/M (and probably influenced by a similar concept in Unix). CP/M was a single-threaded operating system, so there were no race conditions related to the current directory. And at the time, Unix supported only one thread per process, so the issue never arose there either.

The idea of a current directory in today’s multithreaded world is a bit of an anachronism, as if there’s only one “place” a process can be at a time. Standard handles were also designed in a single-threaded world. I think the convention for the current directory should be that only the main thread of the main process can change the current directory: Background threads and helper libraries should keep their hands off.

Bonus chatter: Don’t forget to pass the OFN_NO­CHANGE­DIR flag when you use the common file dialogs, to tell them not to change the current directory.

Bonus reading: The curse of the current directory.

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.

9 comments

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

  • switchdesktopwithfade@hotmail.com

    I’m curious — the environment variable block has a number of pseudovariables that save per-drive paths for some reason — what are these used for, if not current directory?

    Examples:

    =C:=C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\Community\\Common7\\IDE
    =E:=E:\\My Projects

    • Raymond ChenMicrosoft employee Author

      As noted, they are for backward compatibility, so that Windows can pretend that each drive has a current directory (even though it doesn’t). Environment variables are process-global and are therefore subject to the same race conditions as the current directory.

  • Joshua Hudson

    Glibc added per-thread current directory awhile ago for anybody who wants to use it. In the Windows world, the thing is available if you're willing to dig out the Nt Native APIs but Win32 has no such concept. Too bad.

    In any case, both my GUI applications and my services set their current directory to be the application directory at startup; leaving only command line programs to not do so, in which case they never set...

    Read more
    • Jan RingoÅ¡

      NT APIs can set per-thread current directory? Would you know which one?

      • Joshua Hudson

        OBJECT_ATTRIBUTES structure; but it will not back-propagate to Win32 so don’t get your hopes up.

    • Antonio Rodríguez

      Also, the open and save common dialogs change the current directory to the one where the selected file is, at least in classic Visual Basic.

      In the beginning, I used to reset the current directory to the executable directory after each call. But then I evolved to do what Raymond recommends, and started passing only full paths to APIs and file calls. Every file path is parsed by a function called UnpackFilename(), which extends a relative...

      Read more
      • John Elliott

        Ah, happy memories of the time a printer driver decided to change the current directory out from under me…

    • skSdnW

      GetOpenFilename is documented to ignore the NOCHANGEDIR flag and might change the current directory.

      • Joshua Hudson

        I am one more fault from a bad explorer extension away from calling GetOpenFileName out-of-process anyway.