parts/6.36.IgnoredErrorStatusAndUnhandledExceptions-OYB.md

6.36 Ignored Error Status and Unhandled Exceptions [OYB]

6.36.1 Applicability to language

The vulnerabilities described in ISO/IEC 24772-1:2024 6.36 apply to C++, however, C++ provides a mitigation.

C++ includes the C library, especially the header <cerrno> and thus shares C’s issues with the global error-reporting variable errno. See ISO IEC TR 24772-3:2019 6.36 for details and guidance.

In addition to errno, some C++ library features expose error conditions indirectly via a side-effect on the object the operation failed with or via side-effect on a reference parameter. For example, input stream objects will go into a fail state, when formatted input cannot be performed. Without resetting that fail-state of a stream, further input will continue to fail, that can cause further failures when a failure is ignored. Streams provide a non-default mode to throw exceptions on failure. Another example of error reporting via a side-effect is the filesystem library that provides overloads that take a non-const reference of std::error_code.

In general, reporting errors as side-effects, in the worst case via a global state, can easily or accidentally ignore by developers, leading to further consistency problems in the continued execution of the program.

By default, C++ has the C weakness of permitting the call to a function that returns an error code without capturing the return value in a variable.

errnum foo( int a, int b);
. . .
foo(x, y); // failure to capture the return error code.

C++ offers as a mitigating mechanism the [[nodiscard]] attribute. This attribute indicates that the function result must not be discarded. Ignoring the result of a function marked [[nodiscard]] causes a compiler warning.

[[nodiscard]] errnum foo( int a, int b);
. . .
foo(x, y);  // compile error.

if( auto e = foo(a,b); e == 0) { // no compile error
// success
}
else {
// handle errors
}

In addition, the C++ library provides mechanism to extend the return type of a function with extra values for denoting an error. The simplest case is std::optional<T> that extends T with an “empty” state. Callers must check the result of functions returning an optional for the empty state, before accessing its value. This increases the chances that a reported error is detected by the caller. If additional information of the error has to be returned to the caller std::expected<T,AnErrorCode> or alternatively std::variant<T,AnErrorCode> can be used.

C++ offers a set of library-defined exceptions for error conditions that can be detected by checks that are performed by the standard library. In addition, the programmer or libraries that are called by the programmer can define exceptions that are appropriate for the application or for libraries that are used. These exceptions are handled using an exception handler in the environment where the exception occurs or can be propagated out to an enclosing scope.

Functions declared noexcept or noexcept(true) are called noexcept functions. In the majority of cases, destructors are noexcept functions (see ISO IEC 14882 for further information). Any attempt to propagate an exception from a noexcept function, main, or a thread entry point causes abnormal program termination. Marking non-throwing functions noexcept, that are not called from destructors or participate in move operations or swap() overloads, should be carefully considered, because the compiler needs to prepare each call site of such a function for potential abnormal program termination. In case of abnormal program termination, it is implementation-defined whether the destructors for local objects are called (stack unwinding).

Stack unwinding is often responsible for cleanup of external resources. Therefore, abnormal program termination is a vulnerability to be avoided.

See 6.62 Concurrency - Premature Termination [CGS] for issues related to thread or process termination.

An exception propagating out of a coroutine causes the coroutine to end in an unresumable state and the exception is not further propagated.

In C++, when dynamic initialization of a namespace-scoped object fails with an exception, the exception cannot be caught and the program is terminated. Function-scope static objects, in contrast, are initialized the first time execution passes through the declaration and therefore there is always an enclosing function that can catch the exception.

6.36.2 Avoidance mechanisms for language users

To avoid the vulnerability or mitigate its ill effects, C++ developers can: