5 minute read

AccessViolationException
AccessViolationException

When working with unmanaged libraries or legacy COM-based SDKs in C#, developers sometimes encounter one of the most frustrating runtime errors:

Attempted to read or write protected memory. This is often an indication that other memory is corrupt.

These errors usually appear as:

  • AccessViolationException
  • Random process crashes
  • Heap corruption
  • Unstable retry behavior
  • Native memory faults

Unlike standard managed exceptions, these failures originate in unmanaged code, which makes them significantly harder to diagnose and fix.

In this article, we’ll explore the most common causes of memory corruption in COM interop applications and discuss practical strategies to build safer and more stable .NET integrations.

Why COM Interop Requires Extra Care

The .NET runtime automatically manages memory for managed objects using the Garbage Collector (GC). COM components, however, follow very different rules.

COM libraries typically rely on:

  • Native memory allocation
  • Reference counting
  • Apartment-threading models
  • Manual cleanup
  • Thread affinity

This mismatch between managed and unmanaged memory management is where many stability issues begin.

Common Causes of Memory Corruption in C# COM Applications

1. Using Objects After Their Parent Is Disposed

One of the most common causes of crashes is continuing to use a child COM object after its parent resource has already been released.

Example:

var session = Connect();
var document = session.GetDocument();

session.Dispose();

// Native memory already released
document.Read(); // Potential crash

Although the managed wrapper still exists, the underlying unmanaged memory may already be invalid.

2. Calling COM Components from the Wrong Thread

Many COM libraries are apartment-threaded and require execution on an STA (Single Threaded Apartment) thread.

However, common .NET patterns such as:

Task.Run(...)
Parallel.ForEach(...)
BackgroundService

typically execute on MTA (Multi Threaded Apartment) threads.

This mismatch can lead to:

  • Random crashes
  • Corrupted internal state
  • Deadlocks
  • Access violations

Understanding STA vs MTA

COM components often specify a threading model like:

ThreadingModel=Apartment

This means all interactions must occur on the same STA thread.

A safe pattern is to isolate COM work onto a dedicated STA thread.

STA Helper Method

public static T RunOnSta<T>(Func<T> func)
{
    T result = default;
    Exception captured = null;

    var thread = new Thread(() =>
    {
        try
        {
            result = func();
        }
        catch (Exception ex)
        {
            captured = ex;
        }
    });

    thread.SetApartmentState(ApartmentState.STA);

    thread.Start();
    thread.Join();

    if (captured != null)
    {
        ExceptionDispatchInfo
            .Capture(captured)
            .Throw();
    }

    return result;
}

Convenience Overload

public static void RunOnSta(Action action)
{
    RunOnSta<object>(() =>
    {
        action();
        return null;
    });
}

This approach ensures all COM interactions occur inside a valid apartment-threaded environment.

Garbage Collection Is Not Deterministic

Many developers assume COM objects are immediately cleaned up when they go out of scope.

That assumption is incorrect.

Managed wrappers are released only when the GC decides to collect them, which may happen much later than expected.

This can lead to:

  • Native memory leaks
  • Locked files
  • Connection exhaustion
  • Internal cache corruption

Releasing COM Objects Explicitly

For long-running processes or retry-heavy applications, explicit cleanup is often necessary.

Safe COM Release Helper

private static void SafeReleaseCom(object comObject)
{
    if (comObject == null)
        return;

    try
    {
        int remaining;

        do
        {
            remaining = Marshal.ReleaseComObject(comObject);
        }
        while (remaining > 0);
    }
    catch
    {
        // Object may already be released
    }
}

This drains all COM reference counts safely.

Retry Logic Can Accidentally Corrupt Memory

Retry loops are common in enterprise applications, but they become dangerous when live COM objects are reused across retries.

Unsafe example:

for (int i = 0; i < 3; i++)
{
    using var session = CreateSession();

    processor.Process(document);
}

If document was created using a previous session instance, it may internally reference released native memory.

Safer Retry Strategy

Instead of carrying COM objects across retries:

✅ Extract primitive values early

string documentId = document.Id;
string path = document.Path;

❌ Avoid storing live COM wrappers between attempts.

Then recreate COM objects fresh for each retry.

Recommended Retry Pattern

public bool ProcessDocument(
    string documentId,
    out string error)
{
    bool success = false;
    string capturedError = string.Empty;

    for (int attempt = 1; attempt <= 3 && !success; attempt++)
    {
        RunOnSta(() =>
        {
            Session session = null;
            Document document = null;

            try
            {
                session = CreateSession();

                document = session.Load(documentId);

                document.Process();

                success = true;
            }
            catch (Exception ex)
            {
                capturedError = ex.ToString();
            }
            finally
            {
                SafeReleaseCom(document);

                session?.Dispose();
            }
        });

        if (!success)
        {
            Thread.Sleep(attempt * 2000);
        }
    }

    error = capturedError;

    return success;
}

Best Practices for Stable COM Interop

1. Keep COM Object Lifetimes Short

Avoid caching COM wrappers long-term.

Prefer:

  • IDs
  • DTOs
  • Strings
  • Primitive values

over live unmanaged references.

2. Dispose in Reverse Order

Always release child objects before parents.

Correct:

child.Dispose();
parent.Dispose();

Incorrect disposal ordering may invalidate dependent unmanaged resources.

3. Avoid Sharing COM Objects Across Threads

COM wrappers are rarely thread-safe.

Do not pass them into:

  • Async continuations
  • Parallel tasks
  • Queues
  • Background workers

unless explicitly documented as safe.

4. Add Retry Backoff

Rapid retries can amplify memory corruption problems.

Use exponential backoff instead of immediate retries.

Example:

Thread.Sleep(attempt * 2000);

Debugging Native Memory Corruption

Standard managed debugging tools are often insufficient for COM crashes.

The following tools are extremely useful.

Tool Purpose
Visual Studio Native Debugging View unmanaged call stacks
WinDbg + SOS Analyze managed and native memory
Application Verifier Detect heap corruption early
PerfView / ETW Correlate GC activity and crashes
Process Monitor Track file and COM activity

Enable Native Debugging in Visual Studio

To debug unmanaged crashes:

Project → Properties → Debug → Enable native code debugging

This helps reveal the actual native function causing the fault.

Warning Signs of Unsafe COM Usage

You may have a COM lifetime problem if you notice:

  • Random AccessViolationException
  • Intermittent crashes
  • Failures only under load
  • Retry-related instability
  • Persistent file locks
  • Crashes disappearing under debugger
  • Heap corruption errors

Final Thoughts

COM interop remains a critical part of many enterprise systems, especially when integrating with legacy Windows components or unmanaged SDKs.

The key takeaway is:

Managed code safety does not automatically extend to unmanaged libraries.

Building stable COM integrations requires careful attention to:

  • Thread apartments
  • Object ownership
  • Lifetime management
  • Deterministic cleanup
  • Retry boundaries

By following safe threading and disposal patterns, you can significantly reduce memory corruption issues and improve the reliability of your .NET applications.

Leave a comment