Summary
COMObject.Dispose() and COMDynamicObject.Dispose() should be idempotent. Today, disposing the same wrapper more than once can decrement the shared COMProxyShare reference count more than once, which can release the underlying RCW while another NetOffice wrapper still uses it.
Background
NetOffice wraps CLR Runtime Callable Wrappers (RCWs) in COMProxyShare so multiple NetOffice wrapper instances can share one managed COM proxy. COMProxyShare.Release() decrements its own counter and calls Marshal.ReleaseComObject() when the count reaches zero.
The current dispose path sets _isDisposed = true, but it does not return early on later Dispose() calls. COMProxyShare.Release() also decrements _count unconditionally.
Relevant code paths:
Source/NetOffice/COMObject.cs: Dispose(bool disposeEventBinding)
Source/NetOffice/COMDynamicObject.cs: Dispose(bool disposeEventBinding)
Source/NetOffice/COMProxyShare.cs: Release()
Use Case
A caller may dispose an object in more than one cleanup path, or a wrapper may be disposed explicitly and later reached by parent/root cleanup. IDisposable implementations are normally expected to tolerate repeated calls.
Example shape:
var workbook = excel.Workbooks.Add();
var clone = workbook.To<NetOffice.ExcelApi.Workbook>();
workbook.Dispose();
workbook.Dispose(); // should be harmless
clone.SaveAs(path); // can now be using a prematurely released RCW
Problem
The second dispose can decrement the same COMProxyShare count again. If other wrappers still share that RCW, the underlying COM object can be released too early, causing InvalidComObjectException, broken sibling wrappers, or unstable behavior during active COM calls.
Suggested Fix
- Return immediately from
Dispose(bool) when the instance is already disposed or currently disposing.
- Harden
COMProxyShare.Release() against underflow and repeated release.
- Add tests for repeated
Dispose() on a single wrapper and repeated dispose while another wrapper shares the same COMProxyShare.
Summary
COMObject.Dispose()andCOMDynamicObject.Dispose()should be idempotent. Today, disposing the same wrapper more than once can decrement the sharedCOMProxySharereference count more than once, which can release the underlying RCW while another NetOffice wrapper still uses it.Background
NetOffice wraps CLR Runtime Callable Wrappers (RCWs) in
COMProxyShareso multiple NetOffice wrapper instances can share one managed COM proxy.COMProxyShare.Release()decrements its own counter and callsMarshal.ReleaseComObject()when the count reaches zero.The current dispose path sets
_isDisposed = true, but it does not return early on laterDispose()calls.COMProxyShare.Release()also decrements_countunconditionally.Relevant code paths:
Source/NetOffice/COMObject.cs:Dispose(bool disposeEventBinding)Source/NetOffice/COMDynamicObject.cs:Dispose(bool disposeEventBinding)Source/NetOffice/COMProxyShare.cs:Release()Use Case
A caller may dispose an object in more than one cleanup path, or a wrapper may be disposed explicitly and later reached by parent/root cleanup.
IDisposableimplementations are normally expected to tolerate repeated calls.Example shape:
Problem
The second dispose can decrement the same
COMProxySharecount again. If other wrappers still share that RCW, the underlying COM object can be released too early, causingInvalidComObjectException, broken sibling wrappers, or unstable behavior during active COM calls.Suggested Fix
Dispose(bool)when the instance is already disposed or currently disposing.COMProxyShare.Release()against underflow and repeated release.Dispose()on a single wrapper and repeated dispose while another wrapper shares the sameCOMProxyShare.