No Exit Method Call
ID |
csharp.no_exit_method_call |
Severity |
critical |
Remediation Complexity |
trivial |
Remediation Risk |
low |
Remediation Effort |
low |
Resource |
Reliability |
Language |
CSharp |
Tags |
process-termination, reliability, resource-leak |
Description
Reports calls that end the process or the message loop abruptly: Environment.Exit,
Environment.FailFast, Application.Exit and Application.ExitThread. None of them let the
normal shutdown path run, so finally blocks, using disposal and pending flushes are skipped.
Rationale
Returning from Main unwinds the stack: every finally block runs, every using scope disposes
its resource, buffered writers flush, and registered shutdown handlers get their chance. An
explicit termination call skips all of it.
Environment.Exit stops the process where it stands. Environment.FailFast is even more abrupt —
it bypasses managed shutdown entirely and writes a crash dump by design. Application.Exit and
Application.ExitThread tear down the WinForms message loop, abandoning whatever the UI thread was
in the middle of. In every case an open FileStream may keep a partially written file, a database
transaction is left to time out server-side, and a lock file or named mutex is never released.
Termination calls are also untestable and unreusable: a library that calls Environment.Exit on a
bad argument kills its host application, which makes it impossible to unit-test and impossible to
embed. Deciding to stop is the caller’s job; the callee’s job is to report the failure.
Thread.Abort is a different defect and is reported by its own rule.
Calls inside the entry point are not reported. Setting the process exit code there is precisely what
Environment.Exit exists for, and the shutdown the call cuts short is the one that was about to
happen anyway. For a classic entry point the exemption keys on an enclosing method named Main, so
a helper method that happens to carry that name is exempt too. A program written with top-level
statements has no Main to key on — its global statements are the entry point, so a call among
them is exempt as well.
The top-level exemption stops at anything the entry point merely schedules. A call inside a lambda, an anonymous method or a local function of a top-level program runs when that delegate or function is invoked, not as part of the entry point’s own shutdown, so it is still reported.
using System;
using System.IO;
public class Importer
{
public void Run(string path)
{
using (var writer = new StreamWriter(path))
{
writer.Write("header");
if (!File.Exists(path))
{
Environment.Exit(1); // FLAW — writer is never flushed or disposed
}
}
}
public int RunSafely(string path)
{
using (var writer = new StreamWriter(path))
{
writer.Write("header");
if (!File.Exists(path))
{
return 1; // OK — the using scope disposes, Main returns the code
}
}
return 0;
}
public static int Main(string[] args)
{
Environment.Exit(new Importer().RunSafely(args[0])); // OK — exit code set from the entry point
return 0;
}
}
Remediation
Let control return to the entry point and set the exit code there. Throw an exception, or return a
status the caller can act on, and have Main translate it into a return value (or
Environment.ExitCode) after the using and finally blocks have run.
In a WinForms application, close the main form (Form.Close()) instead of calling
Application.Exit, so form-closing handlers and disposal run normally.
Environment.FailFast is defensible in exactly one situation: corrupted process state where
continuing is more dangerous than dying, and a crash dump is wanted. If that is the intent, say so
in a comment at the call site.