createthread-made threads can be mis-recorded in the thread list (TID/handle mismatch); switchthread silently no-ops
Summary
When a thread is created through the createthread command (i.e. CreateRemoteThread while the debuggee is suspended at a breakpoint), its THREADINFO can end up with a wrong ThreadId↔Handle association. After that happens:
switchthread <tid>silently does nothing — no "Thread switched!" message, yet the command reports success;- the Threads view "current" marker and the actual register context disagree;
switchthreadto the mis-recorded thread can activate a different thread's context (register reads/writes then hit the wrong thread).
Threads created by the debuggee itself (CreateThread from the program) are fine; the problem shows up with threads created while the debuggee is suspended, which is exactly what the built-in createthread command does.
Reproduction (x64, Windows 11, development branch build)
- Load any target (e.g.
notepad.exe), stopped at the system breakpoint. - Command bar:
createthread kernel32!GetLastError— note thread A from the log line (Thread ... created at ..., also stored in$result). createthread kernel32!GetLastErroragain — note thread B.switchthread <main tid>— works ("Thread switched!").switchthread B— often produces no message; the Threads view current marker may highlight a different thread than the register context reflects.switchthread A— often silently no-ops: no message andcipstill shows the previously active thread.
In three consecutive sessions, at least one of the two createthread-made threads was mis-recorded every time (which one varies).
Observed matrix (one session, paused at a breakpoint hit by 0x8458)
0x9754 = main thread, 0x8458 and 0x14c0 = createthread-made threads.
| command | "Thread switched!" printed | Threads-view current | register cip |
|---|---|---|---|
switchthread 0x9754 (main) |
yes | 0x9754 | 0x9754's |
switchthread 0x14c0 |
no | 0x8458 | 0x14c0's |
switchthread 0x8458 |
no | 0x9754 | 0x9754's |
Row 2: the register context actually moved, but the current-thread scan (ThreadGetList's threadHandle == hActiveThread walk) matched a different entry — two entries share the stored handle. Row 3: ThreadGetHandle(0x8458) behaves as if it returns the main thread's handle, so the register context stays on main.
Analysis (development branch sources)
ThreadCreate(src/dbg/thread.cpp) identifies the new thread by the event dispatcher's current event instead of the handle it was just given:
curInfo.Handle = CreateThread->hThread;
curInfo.ThreadId = GetDebugData()->dwThreadId; // <- not GetThreadId(CreateThread->hThread)
...
threadList.emplace(curInfo.ThreadId, curInfo); // <- silently keeps the old entry on key collision For threads created while the debuggee is suspended, the CREATE_THREAD_DEBUG_EVENT is queued during the break and processed later; trusting GetDebugData()->dwThreadId at that point is what breaks the ThreadId↔Handle association (matches the symptoms: wrong/duplicate stored handles).
emplacenever repairs a stale entry: if the key already exists, the new THREADINFO is dropped silently.cbDebugSwitchthread(src/dbg/commands/cmd-thread-control.cpp) reports success even when it changes nothing — whenThreadGetHandle(threadid)returns the already-active handle (mis-recorded entry), theif(hActiveThread != newThread)block is skipped without a message andtrueis returned. Same for the call stack view'sswitchthread %1, quiet.
Suggested fix
- In
ThreadCreate, usecurInfo.ThreadId = GetThreadId(CreateThread->hThread);(keepGetDebugData()->dwThreadIdonly as a fallback). - Use
insert_or_assign(or erase + insert) instead ofemplaceso a re-registration can repair a stale entry. - Make
switchthreaddistinguish "switched" from "already active", and warn whenThreadGetId(ThreadGetHandle(threadid)) != threadid(stored-association inconsistency), so automation can detect the bad state instead of writing registers to the wrong thread.
We hit this through bridge/script automation (x64dbg-automate plugin) and are happy to re-run the same scenario against a patch.
Source: x64dbg/x64dbg