The vulnerabilities as specified in ISO/IEC 24772-1 clause 6.59 do not apply to C++, as long as the standard library facilities for creating threads are used. If the C standard library mechanisms or threading libraries from POSIX or other applications are used to create threads, then the vulnerabilities apply as described in ISO/IEC 24772-3 clause 6.59. This subclause will examine the C++ standard mechanisms.
Creating a thread using the std::async
function or the std::thread
or std::jthread
results in the constructor synchronized with the thread creation site. Unless the thread is detached, join()
must be called when using std::thread
or std::terminate
will be invoked upon destruction of the thread object. This can be avoided by using std::jthread
instead.
Failure to create or start a thread due to lack of system resources will cause an exception to be thrown to the creating thread thus the thread object never exists. For the vulnerabilities with unhandled exceptions see clause 6.36 Ignored error status and unhandled exceptions [OYB].
C++ provides other ways to create parallel executing functions via the std::async()
call or the std::packaged_task()
functions. If std::async()
threads are created without a launch policy, then system determines whether or not the thread is run lazily (in the current thread) or in a new thread. The launch policy std::launch::async
or std::launch::deferred
controls whether each asynch thread creation creates new threads or executes the call on demand. When a launch policy is std::launch::async
then each call to std::async()
will always create a new thread which can lead to resource exhaustion. When a launch policy is std::launch::deferred()
the function will execute on the thread that executes the respective get()
function for that async
function.
C++ provides a mechanism to construct and control the execution of a function on a user-controlled potentially asynchronous thread via the std::packaged_task
mechanism that allows access to the result via a std::future
object. Each packaged_task
object can be executed at most once and can be executed on the current thread or on a different thread.
There are a number of vulnerabilities that are possible once the thread is created using std::thread
or std::jthread
. See 6.63 lock protocol errors [CGM]. Each std::thread
object must have its .join()
member function called before the object is destroyed unless its .detach()
member function was previously called. This is not required for threads created as std::jthread
. If .detach()
was called on thread object then any calls to .join()
will result in an exception.
Any exception thrown within a thread’s function needs to be handled by that thread, otherwise such an exception will cause program termination. For handling such termination see clause 6.62 Concurrency - Premature termination [CGS]. Employing a std::packaged_task
object as the thread function allows to pass an exception to the holder of its std::future
object.
A detached thread will execute until its thread function ends or until the program is terminated. The std::jthread
type allows to employ cooperative thread termination through std::stop_token
See 6.60 Concurrency – Directed termination [CGT] or 6.62 Concurrency – Premature termination [CGS].
To avoid the vulnerability or mitigate its ill effects, C++ software developers can:
std::thread::native_handle
, use the avoidance mechanisms of ISO/IEC 24772-1 clause 6.59.5.std::jthread
over std::thread
for explicit thread creation.std::async
or C++ parallel algorithms instead of explicit thread creation and management.std::packaged_task
instead of using std::async()
without specifying a launch policy.std::stop_token
, especially for detached threads.