Skip to content

CWE-404: Improper Resource Shutdown or Release

Overview

Improper resource shutdown occurs when an application fails to close files, database connections, sockets, or other resources after use. The result is resource exhaustion: file descriptor leaks, connection pool depletion, and denial of service.

Relationship to Other CWEs

CWE-404 is a Class-level child of the pillar CWE-664 (Improper Control of a Resource Through its Lifetime). MITRE marks it Allowed with careful review: it maps to a real finding, but only where none of its children fits better.

Which children CWE-404 has depends on the view, and the two that matter here disagree. In Research Concepts (view-1000) there are five: CWE-299, CWE-459, CWE-763, CWE-772 and CWE-1266. In Simplified Mapping (view-1003) - the view a published-vulnerability feed is categorised against - CWE-1266 drops out and CWE-401 appears, giving CWE-401, CWE-459, CWE-763 and CWE-772. CWE-775 is in neither; it is a child only in the two CISQ views, which also add CWE-761 and CWE-762.

The rest have no page yet and are worth filing by number rather than as CWE-404: CWE-772 (Missing Release of Resource after Effective Lifetime), CWE-459 (Incomplete Cleanup) for a release that happens but leaves part of the resource behind, CWE-763 (Release of Invalid Pointer or Reference) for a release aimed at the wrong address, CWE-1266 (Improper Scrubbing of Sensitive Data from Decommissioned Device) for hardware taken out of service with its data still on it, and - from the CISQ views - CWE-775 for a file descriptor or handle, plus CWE-761 and CWE-762 for the two mismatched-free cases.

Risk

Medium-High: Leaked connections, file descriptors and memory accumulate until a pool is empty or the process reaches its descriptor limit, and from that point every request needing one of those resources fails. Files left locked block whatever needs them next, and sensitive data can sit exposed in connections left open.

Remediation Steps

Core Principle: Cleanup must run on every path, including error paths; failing safely includes releasing resources and clearing state.

Locate the Resource Leak

When reviewing security scan results:

  • Start at the reported line and work out which resource is acquired there and where it is meant to be released
  • Note what kind of resource it is: file, database connection, socket, stream, handle
  • Trace the resource's lifecycle through the control flow and find the branches where the close or dispose call is never reached
  • Check what happens on the exception paths, not only the success path

Use Automatic Resource Management (Primary Defense)

Most modern languages provide a construct that releases a resource when the enclosing block exits, even if an exception is thrown: Java's try-with-resources, Python's with statement, C#'s using statement, Go's defer, and C++'s RAII (destructors run on scope exit) are all the same idea.

// SECURE - resource is released automatically when the block exits
with resource = acquire(source):
    use(resource)
// resource is closed here, including on exception

Why this works: Automatic resource management ties cleanup to the language runtime's own control flow instead of a developer remembering to call close() on every path, so it still runs during exceptions, early returns, and any other non-obvious exit.

These constructs run on every exit path the process survives to execute, which is not the same set as "every exit path". A SIGKILL, an OOM kill, a stopped or evicted container, a power loss and a hard crash all skip them, and so does os._exit() or any equivalent that bypasses unwinding. For a file descriptor that is only process-local this costs nothing, because the kernel reclaims it when the process dies, which is why the gap is easy to miss. It costs something whenever the resource has state outside the process that survives it: a pooled database connection the server keeps open, a row-level or advisory lock, a lease, a named semaphore or shared-memory segment, a claimed queue message, a temporary file. Those stay held until something else notices.

So with/using/defer is the right primary defense and needs a second mechanism beside it for that class of resource: a server-side idle timeout or lease expiry, reconciliation at startup that releases anything the previous instance left held, or a supervisor that cleans up when the process disappears. Say which one applies rather than treating the language construct as the whole answer. A shutdown handler does not close this gap either, because SIGKILL and SIGSTOP cannot be trapped at all.

Ensure Cleanup Runs on Every Path

For code that predates automatic resource management, or in languages without it, cleanup must be placed where it always executes regardless of how the block exits:

resource = null
try:
    resource = acquire(source)
    process(resource)
finally:
    if resource is not null:
        try:
            close(resource)
        catch closeError:
            log.warn("close failed", closeError)   // logged, not rethrown - rethrowing here
                                                   // replaces the original exception with this one
  • Place cleanup in a finally-equivalent block whenever an automatic-resource-management construct is unavailable
  • Check for null or unset before closing
  • Catch and log errors from the cleanup call itself rather than rethrowing them
  • A finally block covers every exit the process survives to execute, the same boundary as the automatic constructs above, so a resource with external state still needs the timeout, lease or reconciliation pass described there

Close Resources in Correct Order

  • Close in reverse order of opening (LIFO - Last In, First Out)
  • Work inwards along the chain that produced each handle: a result set or cursor closes before the statement that produced it, and the statement before the connection that produced it
  • In practice that means derived streams before base streams, streams before sockets, and child resources before parent resources

Automatic resource management constructs that accept multiple resources (Java's multi-resource try-with-resources, Python's multiple with context managers) close them in reverse declaration order automatically - lean on that instead of hand-ordering manual close() calls.

Handle Cleanup Exceptions

  • A failure while closing must not replace the exception that caused the failure
  • Log cleanup failures so they remain diagnosable
  • Prefer automatic resource management, which handles suppressed and chained exceptions correctly by default
  • Where the language supports it, attach a cleanup failure to the primary exception (Java's addSuppressed()) instead of discarding one of them

Monitor and Test for Resource Leaks

While testing:

  • Exercise the exception paths explicitly, not only the success path
  • Run load tests to expose connection pool exhaustion
  • Watch file descriptor usage, and run a memory profiler over the same runs

In a running system:

  • Monitor open files, connections and handles over extended periods rather than a single run
  • Use tools like lsof (Linux) or Resource Monitor (Windows)
  • Set up alerts for resource threshold violations

Common Vulnerable Patterns

// No close at all
reader = open_file(file)
line = reader.read_line()
// missing reader.close()

// Close only on the success path
conn = get_connection()
if condition:
    conn.close()  // not always reached

// Exception prevents close from ever running
resource = acquire(source)
process(resource)  // might throw
resource.close()   // never reached if process() throws

Why this is vulnerable: every one of these paths acquires something finite and does not give it back. File descriptors, database connections and sockets are pool-limited, so the cost is cumulative rather than per-request: nothing fails on the first leak or the thousandth, and then the pool is empty and every subsequent request fails, including the ones from users doing nothing wrong. That delay between cause and symptom is why leaks reach production. A load test long enough to exhaust the pool is a different test from the one that proves the feature works.

The second and third shapes are the ones worth checking for specifically, because they leak faster when things go wrong. A close that runs only on the success path, or that sits after a call which can throw, returns the resource whenever the request succeeds and keeps it whenever the request fails. An attacker who can reliably cause an error then drains the pool at the rate they can send requests, turning a resource leak into a denial of service that needs no volume. Acquiring inside the language's scope-bound construct (try-with-resources, using, with, defer, RAII) makes the release unconditional, because it removes the possibility of an exit path that skips it.

Additional Resources