RTL_PERTHREAD_CURDIR

The RTL_PERTHREAD_CURDIR structure (formally _RTL_PERTHREAD_CURDIR) is a small structure whose only known use is for the illusion that 32-bit Windows runs 16-bit Windows programs as 16-bit processes. In fact, 16-bit Windows programs each run as one thread in a specially prepared 32-bit process.

The RTL_PERTHREAD_CURDIR is 0x0C and 0x18 bytes in 32-bit and 64-bit Windows, respectively, in all known versions.

Offset (x86) Offset (x64) Definition
0x00 0x00
RTL_DRIVE_LETTER_CURDIR *CurrentDirectories;
0x04 0x08
UNICODE_STRING *ImageName;
0x08 0x10
PVOID Environment;

From the structure’s name, the intended purpose of the RTL_PERTHREAD_CURDIR is for keeping a current directory for each thread. If indeed the intended threads are each 16-bit programs, then the intended use looks to be that each program can have its own set of current directories for each drive letter. This is consistent with the naming of CurrentDirectories and the type that it points to. From the names of the additional members, this intended use looks to have been extended to take in other properties that are or can be unique to each thread, as with ImageName for the pathname of the 16-bit program and Environment for the program’s environment variables.

Whatever came of any of these intentions is unclear. The only known preparation of an RTL_PERTHREAD_CURDIR is by WOW32.DLL. It builds the structure on the stack of the thread that is the newly running 16-bit Windows program. All that WOW32 sets non-trivially in this structure is the ImageName. Both CurrentDirectories and Environment are initialised to NULL and no further use is known of either. Of course, that no further use is known may mean simply that more research is required.

Repurpose

You might think the RTL_PERTHREAD_CURDIR can be nothing but a historical curiosity. After all, you need some determination these days even to find a 16-bit program, perhaps on a long-forgotten CD (or even a floppy disk), let alone to see one running. Now that almost everyone uses 64-bit Windows, running even one 16-bit Windows program needs more than a little preparation, e.g., of a virtual machine that runs 32-bit Windows.

What may make the RTL_PERTHREAD_CURDIR structure worth attention even in the 2020s is that even if one never gets created, Windows still has code that will use one that ever does get created. Have a look at the TEB. This is the primary storage for whatever the kernel, NTDLL and various low-level user-mode modules use for managing a thread’s user-mode execution. This TEB is a highly variable internal detail, but In all Windows versions the TEB begins with an NT_TIB which is very stable. Among the latter’s members is a SubSystemTib, defined only as pointing to void. All Windows versions have code somewhere that interprets this pointer. To all this code, if SubSystemTib is not NULL, then what it points to is an RTL_PERTHREAD_CURDIR in which to look for the ImageName.

This is true all the way back to Windows NT 3.10. The interpretation is originally in KERNEL32.DLL, both in as prominent a function as the exported GetModuleFileNameW and in an internal routine named BaseComputeProcessDllPath. It stays in GetModuleFileNameW even when moved to KERNELBASE for Windows 7. Windows 8 moved the interpretation to NTDLL, again in one exported function and one internal routine, named LdrGetDllFullName and RtlpGetDirPath, respectively. It’s still there as of the 2004 release of Windows 10—and not just 32-bit, in case the thread is in the host process of a 16-bit Windows program, but in 64-bit Windows too.

Though this use that Windows will make of the RTL_PERTHREAD_CURDIR sounds obscure, it has practical implications for everyday Windows programming. Finding a pathname for “the executable file of the current process” or loading a DLL from the “directory containing the image file used to create the calling process”, as Microsoft puts these things in documentation of the GetModuleFileName and of LoadLibrary functions, is not nearly as well defined as many think (or could ever know from Microsoft’s documentation). The answer you get or the DLL that Windows finds for you—and executes—depends on what the calling thread has for its SubSystemTib.