Skip to content
This repository was archived by the owner on Feb 13, 2026. It is now read-only.

Commit e66a6b5

Browse files
weratcopybara-github
authored andcommitted
[debugger] Fix deadlock in DebugModuleCache
Event handlers in DebugModuleCache may try switching to the main thread, which is dangerous to do while holding a lock. This change address this in two ways: 1) move all event handler invocations outside of the `lock` section 2) don't switch to main thread for calling `IDebugEventCallback2.Send` as it doesn't actually need it. GitOrigin-RevId: b0ceb1592299bd6513fba63578a1b57c291c33e6
1 parent c624804 commit e66a6b5

6 files changed

Lines changed: 89 additions & 72 deletions

File tree

YetiVSI.Shared/DebugEngine/DebugEngineFactoryCompRoot.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ public virtual IDebugEngineFactory CreateDebugEngineFactory()
306306
new LldbAttachedProgram.Factory(
307307
GetJoinableTaskContext(),
308308
GetFactoryDecorator().Decorate<IDebugEngineHandlerFactory>(
309-
new DebugEngineHandler.Factory(GetJoinableTaskContext())),
309+
new DebugEngineHandler.Factory()),
310310
GetTaskExecutor(), eventManagerFactory, debugProgramFactory,
311311
debugModuleFactory, debugThreadAsyncFactory,
312312
debugStackFrameFactory, lldbShell, breakpointManagerFactory,

YetiVSI.Shared/DebugEngine/DebugEngineHandler.cs

Lines changed: 20 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,11 @@
1414

1515
using System;
1616
using System.Collections.Generic;
17+
using System.Diagnostics;
1718
using DebuggerApi;
18-
using Microsoft.VisualStudio.Debugger.Interop;
1919
using Microsoft.VisualStudio;
20-
using System.Diagnostics;
20+
using Microsoft.VisualStudio.Debugger.Interop;
2121
using YetiVSI.DebugEngine.Exit;
22-
using Microsoft.VisualStudio.Threading;
23-
using System.Threading.Tasks;
2422

2523
namespace YetiVSI.DebugEngine
2624
{
@@ -124,26 +122,18 @@ public class DebugEngineHandler : IDebugEngineHandler
124122
{
125123
public class Factory : IDebugEngineHandlerFactory
126124
{
127-
readonly JoinableTaskContext _taskContext;
128-
129-
public Factory(JoinableTaskContext taskContext)
125+
public IDebugEngineHandler Create(
126+
IDebugEngine2 debugEngine, IDebugEventCallback2 eventCallback)
130127
{
131-
_taskContext = taskContext;
128+
return new DebugEngineHandler(debugEngine, eventCallback);
132129
}
133-
134-
public IDebugEngineHandler Create(IDebugEngine2 debugEngine,
135-
IDebugEventCallback2 eventCallback) =>
136-
new DebugEngineHandler(_taskContext, debugEngine, eventCallback);
137130
}
138131

139-
readonly JoinableTaskContext _taskContext;
140132
readonly IDebugEngine2 _debugEngine;
141133
readonly IDebugEventCallback2 _eventCallback;
142134

143-
public DebugEngineHandler(JoinableTaskContext taskContext, IDebugEngine2 debugEngine,
144-
IDebugEventCallback2 eventCallback)
135+
public DebugEngineHandler(IDebugEngine2 debugEngine, IDebugEventCallback2 eventCallback)
145136
{
146-
_taskContext = taskContext;
147137
_debugEngine = debugEngine;
148138
_eventCallback = eventCallback;
149139
}
@@ -155,30 +145,27 @@ public DebugEngineHandler(JoinableTaskContext taskContext, IDebugEngine2 debugEn
155145
// https://docs.microsoft.com/en-us/visualstudio/extensibility/debugger/supported-event-types
156146
public int SendEvent(IGgpDebugEvent evnt, IGgpDebugProgram program, IDebugThread2 thread)
157147
{
158-
return _taskContext.Factory.Run(async () =>
159-
{
160-
return await SendEventAsync(evnt, program, thread);
161-
});
162-
}
163-
164-
public int SendEvent(IGgpDebugEvent evnt, IGgpDebugProgram program,
165-
RemoteThread thread) => SendEvent(evnt, program,
166-
program.GetDebugThread(thread));
167-
168-
async Task<int> SendEventAsync(IGgpDebugEvent evnt, IGgpDebugProgram program,
169-
IDebugThread2 thread)
170-
{
171-
await _taskContext.Factory.SwitchToMainThreadAsync();
172148

173-
if (((IDebugEvent2)evnt).GetAttributes(out uint attributes) != VSConstants.S_OK)
149+
if (evnt.GetAttributes(out uint attributes) != VSConstants.S_OK)
174150
{
175151
Trace.WriteLine($"Could not get event attributes of event ({evnt})");
176152
return VSConstants.E_FAIL;
177153
}
178154

179155
Guid eventId = evnt.EventId;
180-
return _eventCallback.Event(_debugEngine, null, program, thread, evnt, ref eventId,
181-
attributes);
156+
157+
#pragma warning disable VSTHRD010 // Invoke single-threaded types on Main thread
158+
// IDebugEventCallback2.Send doesn't actually require to be called on the main thread.
159+
var ret = _eventCallback.Event(
160+
_debugEngine, null, program, thread, evnt, ref eventId, attributes);
161+
#pragma warning restore VSTHRD010 // Invoke single-threaded types on Main thread
162+
163+
return ret;
164+
}
165+
166+
public int SendEvent(IGgpDebugEvent evnt, IGgpDebugProgram program, RemoteThread thread)
167+
{
168+
return SendEvent(evnt, program, program.GetDebugThread(thread));
182169
}
183170
}
184171
}

YetiVSI.Shared/DebugEngine/DebugModule.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ public int GetInfo(enum_MODULE_INFO_FIELDS fields, MODULE_INFO[] moduleInfo)
152152
info.m_addrPreferredLoadAddress = _lldbModule.GetCodeLoadAddress();
153153
info.dwValidFields |= enum_MODULE_INFO_FIELDS.MIF_PREFFEREDADDRESS;
154154
}
155-
155+
156156
// is used to calculate address's range end
157157
if (HasFlag(enum_MODULE_INFO_FIELDS.MIF_SIZE))
158158
{

YetiVSI.Shared/DebugEngine/DebugModuleCache.cs

Lines changed: 65 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,13 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
using DebuggerApi;
16-
using Microsoft.VisualStudio.Debugger.Interop;
1715
using System;
1816
using System.Collections.Generic;
1917
using System.Diagnostics;
2018
using System.Linq;
19+
using DebuggerApi;
20+
using Microsoft.VisualStudio.Debugger.Interop;
2121
using YetiCommon.CastleAspects;
22-
using YetiVSI.Util;
2322

2423
namespace YetiVSI.DebugEngine
2524
{
@@ -106,8 +105,7 @@ public delegate IDebugModule3 ModuleCreator(SbModule lldbModule, uint loadOrder,
106105
public event EventHandler<ModuleRemovedEventArgs> ModuleRemoved;
107106

108107
readonly ModuleCreator moduleCreator;
109-
110-
Dictionary<SbModule, IDebugModule3> cache;
108+
readonly Dictionary<SbModule, IDebugModule3> cache;
111109
uint nextLoadOrder = 0;
112110

113111
public DebugModuleCache(ModuleCreator moduleCreator)
@@ -118,59 +116,96 @@ public DebugModuleCache(ModuleCreator moduleCreator)
118116

119117
public IDebugModule3 GetOrCreate(SbModule lldbModule, IGgpDebugProgram program)
120118
{
119+
IDebugModule3 module;
120+
bool added = false;
121+
121122
lock (cache)
122123
{
123-
if (!cache.TryGetValue(lldbModule, out IDebugModule3 module))
124+
if (!cache.TryGetValue(lldbModule, out module))
124125
{
125126
module = moduleCreator(lldbModule, nextLoadOrder++, program);
126127
cache.Add(lldbModule, module);
128+
added = true;
129+
}
130+
}
127131

128-
try
129-
{
130-
ModuleAdded?.Invoke(Self, new ModuleAddedEventArgs(module));
131-
}
132-
catch (Exception e)
133-
{
134-
Trace.WriteLine(
135-
$"Warning: ModuleAdded handler failed: {e.Demystify()}");
136-
}
137-
};
138-
return module;
132+
// Event handlers _may_ try to switch to the main thread, which is dangerous to do
133+
// while holding a lock. Therefore we fire them after leaving the critical section.
134+
if (added)
135+
{
136+
try
137+
{
138+
ModuleAdded?.Invoke(Self, new ModuleAddedEventArgs(module));
139+
}
140+
catch (Exception e)
141+
{
142+
Trace.WriteLine(
143+
$"Warning: ModuleAdded handler failed: {e.Demystify()}");
144+
}
139145
}
146+
return module;
140147
}
141148

142149
public bool Remove(SbModule lldbModule)
143150
{
151+
IDebugModule3 module;
152+
bool removed = false;
153+
144154
lock (cache)
145155
{
146-
if (cache.TryGetValue(lldbModule, out IDebugModule3 module))
156+
if (cache.TryGetValue(lldbModule, out module))
147157
{
148158
cache.Remove(lldbModule);
149-
try
150-
{
151-
ModuleRemoved?.Invoke(Self, new ModuleRemovedEventArgs(module));
152-
}
153-
catch (Exception e)
154-
{
155-
Trace.WriteLine(
156-
$"Warning: ModuleRemoved handler failed: {e.Demystify()}");
157-
}
158-
return true;
159+
removed = true;
159160
}
160-
return false;
161161
}
162+
163+
// Event handlers _may_ try to switch to the main thread, which is dangerous to do
164+
// while holding a lock. Therefore we fire them after leaving the critical section.
165+
if (removed)
166+
{
167+
try
168+
{
169+
ModuleRemoved?.Invoke(Self, new ModuleRemovedEventArgs(module));
170+
}
171+
catch (Exception e)
172+
{
173+
Trace.WriteLine(
174+
$"Warning: ModuleRemoved handler failed: {e.Demystify()}");
175+
}
176+
}
177+
return removed;
162178
}
163179

164180
public void RemoveAllExcept(IEnumerable<SbModule> liveModules)
165181
{
182+
var removedModules = new List<IDebugModule3>();
183+
166184
lock (cache)
167185
{
168186
var comparer = SbModuleEqualityComparer.Instance;
169187
var deadModules = cache.Keys.Except(liveModules, comparer).ToList();
170188

171189
foreach (var module in deadModules)
172190
{
173-
Remove(module);
191+
IDebugModule3 debugModule = cache[module];
192+
cache.Remove(module);
193+
removedModules.Add(debugModule);
194+
}
195+
}
196+
197+
// Event handlers _may_ try to switch to the main thread, which is dangerous to do
198+
// while holding a lock. Therefore we fire them after leaving the critical section.
199+
foreach (var module in removedModules)
200+
{
201+
try
202+
{
203+
ModuleRemoved?.Invoke(Self, new ModuleRemovedEventArgs(module));
204+
}
205+
catch (Exception e)
206+
{
207+
Trace.WriteLine(
208+
$"Warning: ModuleRemoved handler failed: {e.Demystify()}");
174209
}
175210
}
176211
}

YetiVSI.Tests/DebugEngine/DebugEngineHandlerTests.cs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
using DebuggerApi;
1616
using Microsoft.VisualStudio;
1717
using Microsoft.VisualStudio.Debugger.Interop;
18-
using Microsoft.VisualStudio.Threading;
1918
using NSubstitute;
2019
using NUnit.Framework;
2120
using System;
@@ -45,18 +44,14 @@ class DebugEngineHandlerTests
4544
[SetUp]
4645
public void SetUp()
4746
{
48-
#pragma warning disable VSSDK005 // Avoid instantiating JoinableTaskContext
49-
var taskContext = new JoinableTaskContext();
50-
#pragma warning restore VSSDK005 // Avoid instantiating JoinableTaskContext
51-
5247
logSpy = new LogSpy();
5348
logSpy.Attach();
5449

5550
program = Substitute.For<IGgpDebugProgram>();
5651
debugEngine = Substitute.For<IDebugEngine2>();
5752
callback = Substitute.For<IDebugEventCallback2>();
5853

59-
handler = new DebugEngineHandler(taskContext, debugEngine, callback);
54+
handler = new DebugEngineHandler(debugEngine, callback);
6055

6156
var idArg = Arg.Any<Guid>();
6257
program.GetProgramId(out idArg)

YetiVSI.Tests/DebugEngine/DebugSessionLauncherTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -492,7 +492,7 @@ DebugSessionLauncher.Factory CreateLauncherFactory(bool stadiaPlatformAvailable,
492492
var symbolSettingsProvider = Substitute.For<ISymbolSettingsProvider>();
493493

494494
var attachedProgramFactory = new LldbAttachedProgram.Factory(
495-
taskContext, new DebugEngineHandler.Factory(taskContext), taskExecutor,
495+
taskContext, new DebugEngineHandler.Factory(), taskExecutor,
496496
new LldbEventManager.Factory(new BoundBreakpointEnumFactory(), taskContext),
497497
new DebugProgram.Factory(taskContext,
498498
new DebugDisassemblyStream.Factory(

0 commit comments

Comments
 (0)