No Thread Suspend Resume

ID

csharp.no_thread_suspend_resume

Severity

critical

Remediation Complexity

trivial

Remediation Risk

low

Remediation Effort

low

Resource

Reliability

Language

CSharp

Tags

concurrency, deadlock, obsolete-api, reliability

Description

Reports calls to Thread.Suspend() and Thread.Resume(). Both have been obsolete since .NET Framework 2.0: they stop a thread at an arbitrary instruction, so the thread can be frozen while holding a lock — the classic unrecoverable deadlock.

Rationale

Suspend does not ask the target thread to stop at a safe point; it stops it wherever it happens to be. That point may be inside a lock block, inside a static constructor, inside the allocator, or in the middle of a framework call that owns an internal lock. Every other thread that needs that lock then blocks indefinitely. There is no timeout, no exception, and no stack that points at the Suspend call — a process dump is the only diagnostic left.

The pair is also useless as a synchronisation primitive, for two reasons. First, the caller has no idea what state the target left behind, so "suspend, read shared data, resume" is guaranteed to observe torn intermediate state some of the time. Second, the calls are not counted or ordered: a Resume racing ahead of its Suspend is lost, and the thread stays suspended forever.

This is why the framework marked both methods obsolete and why .NET Core removed them: on modern targets the call throws PlatformNotSupportedException at runtime. Any occurrence is therefore either dead code on the current target or a latent deadlock on the old one.

using System;
using System.Threading;

public class Coordinator
{
    private readonly ManualResetEventSlim gate = new ManualResetEventSlim(false);

    public void Pause(Thread worker)
    {
        worker.Suspend();                    // FLAW — worker may be frozen holding a lock
    }

    public void Continue(Thread worker)
    {
        worker.Resume();                     // FLAW — races with Suspend, may be lost
    }

    public void WaitForWork()
    {
        gate.Wait();                         // OK — the thread stops at a point it chose
    }

    public void ReleaseWork()
    {
        gate.Set();                          // OK — cooperative signal, no lock can be stranded
    }
}

Remediation

Replace suspension with cooperative signalling: the worker decides where it is safe to stop.

  • ManualResetEventSlim or SemaphoreSlim — the worker calls Wait() at a safe point in its loop and the controller calls Set() / Release().

  • CancellationToken — when the goal is to stop the work rather than pause it, poll ThrowIfCancellationRequested() at the same safe point.

  • Monitor.Wait / Monitor.Pulse — when the pause is tied to a condition over shared state that is already guarded by a lock.

None of these can freeze a thread inside a lock, and all of them make the resume path explicit and idempotent.