1. Changelog
1.1. Revision 0 - December 10th, 2023
-
Initial release. ✨
2. Introduction, Motivation, and Prior Art
During the production of the defer paper, we found some implementations in niche cases performed unwinding. Therefore, as a stopgap, the
paper delegated the behavior of potential unwinding to an extension. It was made implementation-defined whether or not unwinding is completed, with no program-determinable way to handle it.
This is not the best state of affairs, as having an uncheckable form of unwinding means that implementations need not provide any user-actionable way to detect whether or not their implementation is doing unwinding (without onerous
checks or build-time orchestration or potentially fraught macro checks). This is not helpful, especially since
and features such as
may be responsible for sensitive system resources. Accidentally double
-ing or accidentally leaking such critical resources because unwinding is or is not done based on arbitrary, non-code-actionable choices is not helpful to the overall health of the C ecosystem.
This proposal sets out to define a conditionally supported unwinding feature for C, and provide compile-time integer constant expressions through macros in specific headers to allow for a user to know which termination/non-local jump functions will produce behavior that they can rely on. It will also allow them to programmatically devise their own solutions if necessary.
3. Design
The design of this addition is based on a few observations, documented below. Notably,
-
some implementations in very select cases do perform stack unwinding, even in C;
-
that stack unwinding unwinds the versions of
they use on their implementation (such asdefer
);__attribute__ (( cleanup ( …))) -
and, those implementations make the choice at compile-time (not run-time) to do such cleanup.
Therefore, we wanted to provide a conditionally supported, program-checkable way to do stack unwinding. It is our hope that by providing these documenting macros in the various headers (
,
,
, and
), users will get to know exactly which functions will trigger their cascades of
-like features and offer them greater safety and security even in the face of (abnormal) program termination or convoluted non-local jumps and control flow.
3.1. "Why Does This Not Unwind The Whole Call Stack??"
Most C implementations do NOT provide a compiler-driven or library-driven unwinding that we could find, even with
. There is one notable exception, but it requires code to be in "C++ mode" (or have the equivalent of
passed to the compiler to enable it in "C mode"). Right now, calling any of:
-
;exit -
;_Exit -
;quick_exit -
;thrd_exit -
or,
;abort
did not produce any code that called either the cleanup-annotated variables, or other code.
works similarly: no stack unwinding or call stack back-travel is done when any function that refuses to return and returns control to the host environment is done.
Note: This is compatible with C++ semantics for a similar C++ feature: constructors and destructors.
It is noteworthy that not even C++ destructors run on the invocation of any of these functions, either. (You can test that assumption here.) They have to use the C++-specific function
and work with the
in order to get appropriate unwinding behavior. Therefore, there is no precedent — not even from C++ — that C or C++ code should appropriately and carefully unwind the stack.
, therefore, will not provide this functionality. This makes it cheaper and easier to implement for platforms that do not have
, while also following existing practice to the letter. Notably, the "cheapness" and "ease" that will come from the implementation means that at no point will there ever need to be a maintained runtime of unwind scopes or exception handling-alike tables. In fact, no storage of any form of propagation information is necessary for this feature. It simply incentivizes the programming practices currently available to C programs: error codes, structured returns (with error codes embedded), and other testable function outputs in conjunction with better-defined cleanup code.
The one place this does not hold up is
. Consider the following code:
#include <stdlib.h>#include <stdio.h>#include <threads.h>extern void * ep ; extern void * ep2 ; extern int alternate ; void cfree ( void * userdata ) { void ** pp = ( void ** ) userdata ; printf ( "freeing %p !! \n " , * pp ); free ( * pp ); } [[ gnu :: noinline ]] void use ( void * p ) { if (( ++ alternate % 2 ) == 0 ) ep = p ; else ep2 = p ; } int thread ([[ maybe_unused ]] void * arg ) { __attribute__ (( cleanup ( cfree ))) void * p = malloc ( 1 ); printf ( "allocating %p !! \n " , p ); use ( p ); thrd_exit ( 1 ); return 1 ; } int main () { __attribute__ (( cleanup ( cfree ))) void * p = malloc ( 1 ); printf ( "allocating %p !! \n " , p ); int r = 0 ; thrd_t th0 = {}; thrd_create ( & th0 , thread , NULL); thrd_join ( th0 , & r ); use ( p ); exit ( 0 ); return 0 ; } void * ep = 0 ; void * ep2 = 0 ; int alternate = 0 ;
As of December 6th, 2023 on GCC trunk with the latest libpthreads, this code will print:
allocating 0xa072a0 !! allocating 0x7f8034000b70 !! freeing 0x7f8034000b70 !!
with
turned on (or built in C++ mode), and
allocating 0x47e2a0 !! allocating 0x7f7e14000b70 !!
with
not provided. (See it running and change the flags here.) This indicates that, specifically for
and its underlying implementation on
/
, the system will deploy a C++-style exception to do unwinding. This is fine for an implementation, and it is a conforming extension to add unwinding on top of C in this manner (to e.g. be more behavior-compatible with C++ or to protect precious thread-based resources).
However, note that even in this example, the memory from
is always leaked, no matter what. This means that even in C++ mode or C mode with
specified,
,
, and similar do not provide unwinding capabilities. Implementations should feel free to change or enhance this behavior.
Finally, we note that pretty much everything in MSVC is done by doing stack unwinding with their Structured Exception Handling (SEH) or similar techniques, so for the macros we provide almost every single one will be defined and have the value of
. This includes even
.
4. Implementation Experience
MSVC performs select types of stack unwinding with
and
, even in C. glibc (but not musl-libc or µlibC or really any other libc) implements their
/
behavior as a thrown exception when the compiler detects
or C++ mode is enabled. Otherwise, most other implementations do not perform any kind of stack unwinding.
The reason we provide so many different macros is because implementations have, effectively, chosen what happens for these on a function-by-function basis: therefore, the best we can do to provide good standards-backed, implementation-defined/conditionally supported behavior is to mention it directly in the paper.
5. Wording
Wording is relative to the latest draft revision of the C Standard.
5.1. Add a new §5.1.2.5 Unwinding describing the Conditionally Supported unwinding semantics
5.1.2.5 UnwindingUnwinding is a conditionally supported feature of executing statements and expressions as the program returns to a specific location through a non-local jump, or through the program termination. There is:
partial unwinding, when a program or thread is not terminated and the program returns to some location within itself and on the same thread;
thread unwinding, when a program is not terminated but a thread is terminated;
or, program unwinding, when a program is normally or abnormally terminated.
Unwinding is a conditionally supported feature. Support is queried by checking the following macro definitions from Clause 7:S
(
__STDC_LONGJMP_UNWINDS__ , 7.13)
< setjmp . h >
(
__STDC_SIGNAL_SIGABRT_UNWINDS__ , 7.14)
< signal . h >
(
__STDC_SIGNAL_SIGFPE_UNWINDS__ , 7.14)
< signal . h >
(
__STDC_SIGNAL_SIGILL_UNWINDS__ , 7.14)
< signal . h >
(
__STDC_SIGNAL_SIGINT_UNWINDS__ , 7.14)
< signal . h >
(
__STDC_SIGNAL_SIGSEGV_UNWINDS__ , 7.14)
< signal . h >
(
__STDC_SIGNAL_SIGTERM_UNWINDS__ , 7.14)
< signal . h >
(
__STDC__EXIT_UNWINDS__ , 7.24)
< stdlib . h >
(
__STDC_ABORT_UNWINDS__ , 7.24)
< stdlib . h >
(
__STDC_EXIT_UNWINDS__ , 7.24)
< stdlib . h >
(
__STDC_QUICK_EXIT_UNWINDS__ , 7.24)
< stdlib . h >
(
__STDC_THRD_EXIT_UNWINDS__ , 7.28.1)
< threads . h > It is implementation-defined if other features or functions provide unwinding semantics. When supported, specific function calls or actions specified in this document or by the implementation trigger unwinding.
For partial unwinding, a program that performs a non-local jumps from one block into another block runs every currently reached but unexecuted
statement (6.8.1), in the order and with the semantics as specified in 6.8.1, that has been reached between the current execution path (including recursive function invocations) and the location being jumped to.
defer For thread unwinding, a program that performs the termination of a single thread of execution runs every currently reached but unexecuted
statement (6.8.1), in the order and with the semantics as specified in 6.8.1, that has been reached between the current execution path (including recursive function invocations) and the start of the execution of the thread.
defer For program unwinding, a program that terminates (normally or abnormally) runs every reached but currently unexecuted
statement, in the order and with the semantics as specified in 6.8.1, that has been reached between the current execution path (including recursive function invocations) and the start of the program.
defer When not supported, none of the actions described in the preceding paragraphs of this section are taken.
5.2. Modify §6.8.7 Defer statements describing the Conditionally Supported unwinding semantics
6.8.7 Defer statementsIf E has any defer statements D that have been reached and their S have not yet executed, but the program is terminated or leaves *E through any means such as:
a function with the deprecated
function specifier, or a function annotated with the
_Noreturn /
no_return attribute, is called;
_Noreturn or, any signal
,
SIGABRT , or
SIGINT occurs;
SIGTERM then any such S are not run,
unless as specified otherwise by the implementationFN0✨)except indicated by the conditional support for unwinding (5.1.2.5) . Any other D that have not been reached are not run.FN0✨)The execution of deferred statements upon non-local jumps or program termination is a technique sometimes known as "unwinding" or "stack unwinding", and some implementations perform it. See also ISO/IEC 14882 Programming languages — C++, section [except.ctor].
5.3. Add a new paragraph 3 of §7.13 to describe one of the conditionally supported unwinding macros
7.13 Non-local jumps
< setjmp . h > …The macro
__STDC_LONGJMP_UNWINDS__ is an integer constant expression with a value equivalent to 1 if partial unwinding (5.1.2.5) is supported when the
function is invoked successfully, or 0 otherwise.
longjmp …
5.4. Add a new paragraph to §7.14 to describe several of the conditionally supported unwinding macros
7.14 Signal handling
< signal . h > …The macros
__STDC_SIGNAL_SIGABRT_UNWINDS__ __STDC_SIGNAL_SIGFPE_UNWINDS__ __STDC_SIGNAL_SIGILL_UNWINDS__ __STDC_SIGNAL_SIGINT_UNWINDS__ __STDC_SIGNAL_SIGSEGV_UNWINDS__ __STDC_SIGNAL_SIGTERM_UNWINDS__ are integer constant expressions with a value equivalent to 1 if unwinding (5.1.2.5) is supported when the signals
,
SIGABRT ,
SIGFPE ,
SIGILL ,
SIGINT , or
SIGSEGV are raised, respectively, or 0 otherwise.
SIGTERM …
5.5. Modify paragraph 4 of §7.24 to describe several of the conditionally supported unwinding macros
7.24 General utilities
< stdlib . h > ……
which is never greater than
; and,
MB_LEN_MAX __STDC__EXIT_UNWINDS__ __STDC_ABORT_UNWINDS__ __STDC_EXIT_UNWINDS__ __STDC_QUICK_EXIT_UNWINDS__ are integer constant expressions with a value equivalent to 1 if program unwinding (5.1.2.5) is supported when the functions
,
_Exit ,
abort , or
exit are invoked and terminate the program, respectively, or 0 otherwise.
quick_exit …
5.6. Modify paragraph 3 of §7.28.1 to describe one of the conditionally supported unwinding macros
7.28 Threads
< threads . h > 7.28.1 Introduction……
which is never greater than
; and,
MB_LEN_MAX __STDC_THRD_EXIT_UNWINDS__ is an integer constant expression with a value equivalent to 1 if thread unwinding or program unwinding (5.1.2.5) is supported when the function
is invoked and terminates the thread or program, or 0 otherwise.
thrd_exit …
5.7. Modify Annex J’s list of implementation-defined behaviors
Note: 📝 For the editor to do within the Annex J implementation-defined behavior list.