ICorDebug (ICD) in managed-only debugging mode does not need to be a reentrant API. In other words, you can imagine all of ICorDebug being under a single giant monitor (lock). This also means that all calls to ICorDebug will return without blocking on another ICorDebug call. Note, I'm talking about managed-only debugging; this is all out the window for interop-debugging because of out-of-band events.
It helps that ICorDebugProcess::Stop is the only blocking call in ICorDebug. You'll notice all other calls are asynchronous (one example is ICorDebugStepper). It's true that many calls in ICorDebug will forward on to a helper thread in the debuggee, but that thread is unblocked and so the original ICorDebug call is unblocked. Stop() is synchronous and will block until the debuggee is stopped. In retrospect, we think this was a bad idea. Stop() should have been asynchronous and returned immediately. A debug event should have been fired once the runtime was stopped. That would be more consistent with the rest of our eventing model.
It also means that a single thread could own the ICorDebug instances and service all calls to ICD from other threads. ICD has an auxiliary thread that dispatches callbacks for managed debug events. Any calls to ICorDebug from this thread could be forwarded to the owning thread.
It turns out that we managed the locking ourselves and two threads may both happen to be inside of ICorDebug, but that is a detail. We plan to explore more concurrency as needed.