On Mon Feb 16 14:31:37 2026 +0000, Rémi Bernon wrote:
I didn't mean that these flags were usable from user space or that they would hide the thread at all, but rather that they seem to indicate that more threads may be present at program startup, for DLL loading purposes. More research [^1] [^2] [^3] [^4] seems to indicate that it is the case indeed on Windows 10. This sample application also shows an extra thread being present as early as main executable TLS callbacks are executed (idk if there's any user code that can execute earlier than that): ```C #include <stdio.h> #include <ntstatus.h> #define WIN32_NO_STATUS #include <windef.h> #include <winbase.h> #include <winternl.h> void __stdcall tls_callback( void *dll_handle, int reason, void *reserved ) { HANDLE thread = 0, prev = 0; UINT status; while (!(status = NtGetNextThread( GetCurrentProcess(), prev, THREAD_ALL_ACCESS, OBJ_INHERIT, 0, &thread ))) { void *entry = (void *)0xdeadbeef; THREAD_BASIC_INFORMATION info; NtClose( prev ); status = NtQueryInformationThread( thread, ThreadBasicInformation, &info, sizeof(info), NULL ); if (status) printf( "ThreadBasicInformation status %#x\n", status ); status = NtQueryInformationThread( thread, ThreadQuerySetWin32StartAddress, &entry, sizeof(entry), NULL ); if (status) printf( "ThreadQuerySetWin32StartAddress status %#x\n", status ); printf( "%p:%p teb %p entry %p\n", info.ClientId.UniqueProcess, info.ClientId.UniqueThread, info.TebBaseAddress, entry ); prev = thread; } if (status && status != STATUS_NO_MORE_ENTRIES) printf( "NtGetNextThread status %#x\n", status ); if (prev) NtClose( prev ); do { static volatile int i = 0; while (!i) {} } while (0); } static const void *tls_callbacks[] = { tls_callback, 0 }; static int tls_index; IMAGE_TLS_DIRECTORY _tls_used = { 0, 0, (UINT_PTR)&tls_index, (UINT_PTR)tls_callbacks }; int main(int argc, char* argv[]) { do { static volatile int i = 0; while (!i) {} } while (0); } ``` Process explorer also shows the same thing, with an extra thread being there at process startup, doing some ntdll internal work. Also fwiw neither this thread or the main thread have executable entry point as `ThreadQuerySetWin32StartAddress` entry, but rather some ntdll functions, to the contrary to what we do in Wine. IMO this tends to indicate that having an internal thread present on process startup is not so bad. We might need to make it go through ntdll PE side to make sure it has some PE side frames but we can also maybe question whether this is actually necessary. [^1]: https://stackoverflow.com/questions/78458827/how-to-correctly-determine-a-pr... [^2]: https://stackoverflow.com/questions/42789199/why-there-are-three-unexpected-... [^3]: https://bugzilla.mozilla.org/show_bug.cgi?id=1515088 [^4]: https://searchfox.org/firefox-main/source/security/sandbox/chromium-shim/pat... Checking at TLS callback is too late. At this point ntdll loader is initialized and TLS callbacks for dependencies is already executed. User app may start the process suspended and inject a hook for LdrInitializeThunk, or just check the extra threads from the other (e. g., launcher) process. A clean and probably easiest way to check if there are any threads early is to first start process suspended and enumerate the threads in it from the launcher. Maybe worth checking to be sure, but IIRC I did that in the past and there should be no such.
Then, if we want to mind LdrpWorkCallback() in some way I think we should first implement that doing the same as on Windows. It is not so easy to do, I think it will first need to redo loader locking in a modern Windows launcher way (I attempted once in the context of the other rare issues current extra locking is causing but initial attempt was too bad, that needs to be redone differently starting from managing modern Windows DLL load status evolution). And then I doubt those threads can be abused for this purpose, those do not persist throughout (or maybe it can even, but it should be explored in details first what they do and how can be used). But that is quite some work to implement properly first, and that is definitely not something which would create an extra thread from kernel before LdrInitializeThunk is even executed first time. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10058#note_129739