On Mon Feb 16 14:26:40 2026 +0000, Paul Gofman wrote:
Does THREAD_CREATE_FLAGS_LOADER_WORKER somehow hide thread completely on Windows? I doubt so, probably there is no way to hide user space thread completely. Loader is a different aspect. Threads may skip loader but they are still visible through NtGetNextThread or NtQuerySystemInformation( SystemProcessInformation ). So I am not 100% sure that Windows doesn't have such a way but doubt it does. 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( (void *)-1, 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); } ``` 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... -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10058#note_129737