IDisposable not disposed
| Vulnerability potential | Medium |
| DDoS potential | Medium |
The resource is declared without using/using var and without a try-finally Dispose; the OS handle / connection / buffer leaks on early return or exception
Impact
An IDisposable (a file handle, socket, SqlConnection, HttpResponseMessage, Stream, registry key, native buffer, OS sync object, etc.) is created but never deterministically released: there is no using/using var and no try/finally that calls Dispose. On the happy path the object may eventually be collected, but on an early return or an exception between creation and any manual Dispose, the resource is abandoned. It is freed only if and when the GC runs a finalizer — and only if the type has one.
Concrete consequences:
- Handle / descriptor exhaustion. Leaked file, socket, and pipe handles accumulate until the process hits its OS handle limit, after which every new
open/connect/acceptfails. - Connection-pool starvation. ADO.NET and HTTP connections come from bounded pools. Undisposed
SqlConnection/DbConnectionobjects keep pool slots checked out until a finalizer (if any) returns them, so the pool drains and callers block until the pool timeout, then throwInvalidOperationException(“Timeout expired … pool”). The whole data tier appears hung. - Held locks / file locks. A leaked
FileStreamkeeps an exclusive OS lock, so other code or other processes cannot open or delete the file; a leaked mutex/semaphore can deadlock waiters. - Memory growth. Undisposed objects (and any native memory they pin) stay alive at least until the next GC + finalization; for types with no finalizer the native resource is never reclaimed for the life of the process.
Because these are bounded, shared resources, a steady trickle of leaks degrades into a hard failure (DoS) under sustained load.
Vulnerability potential
Resource leaks have genuine security weight (Medium for both ratings) because exhausting a bounded resource is a denial-of-service primitive, and the held resources can have confidentiality/integrity side effects.
- Resource-exhaustion DoS. If an attacker can drive the leaky path — open many connections, send many requests that each leak a handle, trigger the exception branch repeatedly — leaked handles or pooled connections accumulate until the limit is hit. Then legitimate requests fail (handle limit, “connection pool timeout”, inability to open files). A small, cheap request that leaks one descriptor each time is an amplified DoS.
- Lock-based DoS / deadlock. A leaked exclusive file lock or unreleased mutex/semaphore blocks other operations or processes indefinitely, hanging functionality without crashing it.
- Stale state and information exposure. Connections, transactions, and temp files left open past their intended scope can keep sensitive data resident, hold database transactions/locks open, or leave temp files on disk longer than expected. Undisposed crypto objects may keep key material in memory longer than necessary.
These map to CWE-401 (missing release of resource) and CWE-772 (missing release after effective lifetime). The realistic worst case is DoS; data-exposure outcomes are situational, which keeps this at Medium rather than High.
Technical details
Deterministic disposal vs the finalizer/GC
The .NET GC reclaims managed memory, but it knows nothing about OS handles, sockets, or unmanaged buffers, and it runs only under memory pressure — not when a handle limit is hit. IDisposable.Dispose is the deterministic release mechanism: it runs now, at a point you control, freeing the resource immediately. The whole IDisposable contract exists precisely because GC timing is the wrong signal for non-memory resources.
The finalizer is a backstop, not a plan
Some disposable types (those wrapping native handles, typically via SafeHandle) also implement a finalizer so the OS resource is eventually freed even if you forget to Dispose. But:
- Finalization is non-deterministic and can lag far behind: objects go on the finalization queue at the first GC and are only finalized on a later pass by a single finalizer thread. Under load, leaked resources pile up faster than they are reclaimed.
- A finalizer that frees a pooled connection still leaves the slot checked out until that finalizer runs — by which time the pool may already be exhausted.
- Not every disposable has a finalizer. Many wrappers (and types holding managed disposables only) have none, so a missed
Disposeis never compensated — the resource leaks for the entire process lifetime.
So relying on finalization is, at best, delaying the failure, and at worst, a permanent leak.
using = try/finally
using var s = Open(); (and the block form) compiles to a try { ... } finally { s?.Dispose(); }, guaranteeing Dispose runs on every exit — normal completion, early return, or exception. That is exactly what the leaky code omits. For fields, the Dispose pattern (IDisposable on the owning type, disposing owned members in its Dispose) carries the same guarantee up the ownership chain. For IAsyncDisposable, use await using.
Catching the issue
Roslyn / .NET analyzers
- CA2000 (Dispose objects before losing scope) is the primary rule: it flags a local
IDisposablethat can go out of scope withoutDisposebeing called on all paths, including the exception path. This is the direct detector for this defect. - CA2213 (Disposable fields should be disposed) catches the field-ownership variant: a type holds a disposable member but its own
Disposedoesn’t dispose it. - CA1816 is related to correct
Disposeimplementation (callGC.SuppressFinalize(this)), relevant when you write the owning type’sDispose. - IDE0063 / IDE0067 / IDE0068 / IDE0069 suggest
usingdeclarations and warn about disposable locals/fields not being disposed. - Promote CA2000/CA2213 to warnings-as-errors in
.editorconfigso a missingusingcannot merge.
SonarQube
Rule S2930 (“IDisposable/IAsyncDisposable objects should be disposed”) and S3881 (Dispose pattern correctness) flag undisposed locals and incorrect Dispose implementations.
CodeQL
The cs/local-not-disposed (and resource-leak) queries trace data flow from a disposable allocation to all exits and report paths with no Dispose, including across early returns and exception edges.
Nullable / review practices
- Make
using varthe default for any local that implementsIDisposable; treat a barevar x = new SomethingDisposable()without ausingas a review flag. - Be careful not to dispose objects you don’t own — notably the singleton
HttpClient, or aStream/connection handed in by a caller. Use theleaveOpenconstructor arguments where applicable. - Stress-test under load and watch process handle count and connection-pool counters; a monotonically rising handle count is the runtime signature of this bug.
How to reproduce
Observe that the leaky method abandons the FileStream on the exception path (the file stays locked / the handle leaks), while the using version always releases it.
using System;
using System.IO;
class Program
{
// BUG: if Validate throws, 'fs' is never disposed -> handle + exclusive file lock leak.
static void WriteLeaky(string path, byte[] data)
{
FileStream fs = File.Open(path, FileMode.Create, FileAccess.Write, FileShare.None);
Validate(data); // throws -> early exit, fs.Dispose() never reached
fs.Write(data, 0, data.Length);
fs.Dispose(); // only runs when nothing above threw
}
// FIX: using guarantees Dispose on every path (return, throw, normal).
static void WriteSafe(string path, byte[] data)
{
using var fs = File.Open(path, FileMode.Create, FileAccess.Write, FileShare.None);
Validate(data);
fs.Write(data, 0, data.Length);
}
static void Validate(byte[] data)
{
if (data.Length == 0) throw new ArgumentException("empty");
}
static void Main()
{
try { WriteLeaky("leak.tmp", Array.Empty<byte>()); } catch { }
// The handle from WriteLeaky is now leaked until a finalizer (if any) runs.
// This open with FileShare.None can fail because the leaked stream still holds the lock.
try
{
using var probe = File.Open("leak.tmp", FileMode.Open, FileAccess.Write, FileShare.None);
Console.WriteLine("reopened OK");
}
catch (IOException ex)
{
Console.WriteLine($"still locked by leaked handle: {ex.Message}");
}
}
}