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

Raymond Chen

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.

9 comments

Comments are closed. Login to edit/delete your existing comments

  • Joshua Hudson 0

    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 their current directory but rather inherit it from the calling process; thus interpret relative paths on the command line or response file correctly.

    Thus I can use relative paths at will and not worry about the thread race in GetFullPathName because it will not come up.

    (Pedantic stupid comment: Do not inject a thread into another process and call SetCurrentDirectory. That’s not yours!)

    • skSdnW 0

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

      • Joshua Hudson 0

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

    • Antonio Rodríguez 0

      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 path based on an explicit origin, which, depending on where the relative path is coming from, can be the executable directory, the application’s directory in Program Data, or the directory containing the document opened by the user.

      Classic Visual Basic is single-threaded (you can create multiple threads, but as there is no support from the runtime, you are on your own with the Windows API and must take care of synchronization yourself), but the current directory can be changed by COM objects, ActiveX controls, DLLs (injected or explicitly loaded) or even third-party modules imported into your project, so relying on it being correctly set only gives headaches and Heisenbugs. Yes, it’s rude changing the drapes of the home you are a guest to, but that doesn’t stop people from doing it…

      • John Elliott 0

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

    • Jan Ringoš 0

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

      • Joshua Hudson 0

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

  • switchdesktopwithfade@hotmail.com 0

    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 0

      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.

Feedback usabilla icon