Wrong Lock Usage
ID |
csharp.wrong_lock_usage |
Severity |
critical |
Remediation Complexity |
trivial |
Remediation Risk |
low |
Remediation Effort |
low |
Resource |
Reliability |
Language |
CSharp |
Tags |
concurrency, reliability |
Description
Reports calls to Monitor.Enter(x) or Monitor.TryEnter(x) whose monitor is not
reliably given back. Two families are covered: the acquiring method never releases the
monitor at all, and the method does release it but not from a finally clause, so an
exception or an early return can skip the release.
Rationale
Monitor.Enter acquires a synchronization lock that must be released via
Monitor.Exit. The lock statement does this automatically by emitting the equivalent
of try { Monitor.Enter(x); … } finally { Monitor.Exit(x); }. Code that calls
Monitor.Enter directly must reproduce that pattern.
Acquired but never released in the same method. A lock has to be released by the very
method that took it. When a method acquires a monitor and then simply returns — or leaves
the release to a separate Release() style method that callers may forget, may call in
the wrong order, or may never reach because an exception unwound the stack — the monitor
stays held for the lifetime of the process and every later acquirer deadlocks. Pairing
acquire and release inside one method is what makes the pairing verifiable at all: a
reader of the method can see both ends of the critical section.
Released, but not on every exit path. Even when the release lives in the same method,
placing it after the critical section instead of inside a finally means any exception
thrown in between — or any return, break or continue that jumps over it — leaks the
lock exactly as if it were never written.
public void NeverReleased(object sync)
{
Monitor.Enter(sync); // FLAW — nothing in this method releases the monitor
DoWork();
}
public void ReleasedElsewhere(object sync)
{
Monitor.Enter(sync); // FLAW — the release is another method's problem
}
public void Release(object sync)
{
Monitor.Exit(sync); // callers may never reach this
}
public void SkippableRelease(object sync)
{
Monitor.Enter(sync); // FLAW — no try/finally guarding the release
DoWork();
Monitor.Exit(sync); // skipped if DoWork() throws
}
public void Good(object sync)
{
Monitor.Enter(sync); // OK — paired with Monitor.Exit in finally
try
{
DoWork();
}
finally
{
Monitor.Exit(sync);
}
}
public void Best(object sync)
{
lock (sync) // OK — compiler emits the try/finally pair
{
DoWork();
}
}
Remediation
Prefer the lock statement whenever possible — the compiler generates the correct
try/finally automatically, and it makes acquire and release structurally inseparable.
If Monitor.Enter must be called directly (for example to use TryEnter with a
timeout), release the monitor in the same method and do it from a finally:
Monitor.Enter(sync); try { … } finally { Monitor.Exit(sync); }. Never split the
acquire and the release across two methods — no caller can be relied on to invoke the
releasing method on every path, exceptional or not.