Skip to content

Make COMObject and COMDynamicObject Dispose idempotent when sharing COMProxyShare #479

@jozefizso

Description

@jozefizso

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    Status
    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions