Sometimes ICorDebug APIs will return CORDBG_E_OBJECT_NEUTERED. If you're using MDbg, COM-interop will convert this to throwing an exception.
This means that the object is "logically dead". Since somebody is holding an outstanding reference to it, we can't actually delete it. But we can release all of its external resources and fail all method calls on it.
What does it mean?
A neutered object has the following properties:
1) All external resource for the object are freed, including references to other objects. For example, a neutered ICorDebugModule does not hold any file locks.
2) Most methods on a neutered object will return this failure code. A few select hand-picked methods (that don't require any external resources to succeed) will still succeed on neutered objects. For example, ICDProcess::GetID will work on a neutered process; ICDProcess::GetHandle will fail. In short: don't expect to be able to do anything interesting on a neutered object.
3) All of its logical 'children' objects are neutered. For example, a module is logically a child of an appdomain. See here for more parent-child relationships in ICorDebug.
The most common time this happens is after resuming the debuggee by calling ICorDebugProcess::Continue() (which is what MDbgProcess.Go() does under the covers). In that case, everything associated with your callstacks gets neutered, including all Frames, Chains, and basically every ICorDebugValue that's not actually an ICorDebugHandleValue.
This also happens after various Exit/Unload callbacks, the ICorDebug object in question will get neutered. For example, after an ExitAppDomain event, everything in that appdomain (ICorDebugAssembly, ICDModule, ICDType, ICDFunction, ICDCode objects) all get neutered.
The naive thing is to have an object completely alive until its final reference is released. But addref/release doesn't solve all the problems. Neutering has several advantages:
- AddRef/Release is very nondeterministic. The client (in our case, debuggers like VS or MDbg via com-interop) can have as many references as they want and release them whenever they want. In practice, the product gets trained to a certain pattern of releases. This became especially bad when we started doing MDbg, because com-interop is very random about when it calls Release (since it's coupled with the GC's non-determinism).
- simplify shutdown. Shutdown is evil and we want to simplify it as much as possible. It's once thing when a debugger calls the final release on ICorDebugModule after the Module unload event. It's another thing when it holds onto it and calls Release() after the entire debuggee has shutdown. We didn't want to create different shutdown paths for every random order that the client debugger may release objects.
- protect yourself against leaking references. We had problems where the debugger would forget a release on something like an ICorDebugModule, and thus ICorDebug would hold onto a file-lock. This would prevent you from recompiling your project and thus could force you to exit your debugger. Ug. This gets worse because maybe the leak was of an IFoo, which has a ref to an IBar, which has a ref to the ICorDebugModule. This can also be a major issue if your debugger loads an add-in, and that add-in is the one that leaks the reference. So although leaking a reference is a bug, it's still nice to fortify your subsystems enough to mitigate damage from bugs.
- enforcement. We neuter objects that become logically dead as a way to enforce that debuggers don't still try to use them. For example, once you continue, all threads continue to run and so all the ICorDebugFrame objects becomes invalid. So what happens if you try to get the locals from an invalid object? We need to fail somehow.