N3198
Conditionally Supported Unwinding

Published Proposal,

Previous Revisions:
None
Authors:
Paper Source:
GitHub
Issue Tracking:
GitHub
Project:
ISO/IEC 9899 Programming Languages — C, ISO/IEC JTC1/SC22/WG14
Proposal Category:
Change Request, Feature Request
Target:
C2y/C3a

Abstract

Many compilers’s "cleanup" attribute has long-since provided scope-based, compile-time deterministic, well-known mechanism for the C language to clean up resources of all kinds (not just memory). This proposal attempts to standardize something as close to existing practice as possible while providing a select and measured few set of behaviors to ensure greater portability and usability in the C ecosystem.

1. Changelog

1.1. Revision 0 - December 10th, 2023

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 defer 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 autoconf checks or build-time orchestration or potentially fraught macro checks). This is not helpful, especially since defer and features such as __attribute__((cleanup())) may be responsible for sensitive system resources. Accidentally double free-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,

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 (<stdlib.h>, <threads.h>, <setjmp.h>, and <signal.h>), users will get to know exactly which functions will trigger their cascades of defer-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 __attribute__((cleanup())) p = malloc(2);. There is one notable exception, but it requires code to be in "C++ mode" (or have the equivalent of -fexceptions passed to the compiler to enable it in "C mode"). Right now, calling any of:

did not produce any code that called either the cleanup-annotated variables, or other code. defer 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 std::terminate() and work with the std::terminate_handler 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. defer, therefore, will not provide this functionality. This makes it cheaper and easier to implement for platforms that do not have __attribute__((cleanup())), 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 thrd_exit. 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 -fexceptions turned on (or built in C++ mode), and

allocating 0x47e2a0 !!
allocating 0x7f7e14000b70 !!

with -fexceptions not provided. (See it running and change the flags here.) This indicates that, specifically for thrd_exit and its underlying implementation on pthread_cancel/pthread_exit, 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 main is always leaked, no matter what. This means that even in C++ mode or C mode with -fexceptions specified, exit, quick_exit, 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 1. This includes even longjmp.

4. Implementation Experience

MSVC performs select types of stack unwinding with __try and __finally, even in C. glibc (but not musl-libc or µlibC or really any other libc) implements their pthread_cancel/pthread_exit behavior as a thrown exception when the compiler detects -fexceptions 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 Unwinding

Unwinding 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__ (<setjmp.h>, 7.13)

  • __STDC_SIGNAL_SIGABRT_UNWINDS__ (<signal.h>, 7.14)

  • __STDC_SIGNAL_SIGFPE_UNWINDS__ (<signal.h>, 7.14)

  • __STDC_SIGNAL_SIGILL_UNWINDS__ (<signal.h>, 7.14)

  • __STDC_SIGNAL_SIGINT_UNWINDS__ (<signal.h>, 7.14)

  • __STDC_SIGNAL_SIGSEGV_UNWINDS__ (<signal.h>, 7.14)

  • __STDC_SIGNAL_SIGTERM_UNWINDS__ (<signal.h>, 7.14)

  • __STDC__EXIT_UNWINDS__ (<stdlib.h>, 7.24)

  • __STDC_ABORT_UNWINDS__ (<stdlib.h>, 7.24)

  • __STDC_EXIT_UNWINDS__ (<stdlib.h>, 7.24)

  • __STDC_QUICK_EXIT_UNWINDS__ (<stdlib.h>, 7.24)

  • __STDC_THRD_EXIT_UNWINDS__ (<threads.h>, 7.28.1)

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 defer 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.

For thread unwinding, a program that performs the termination of a single thread of execution runs every currently reached but unexecuted defer 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.

For program unwinding, a program that terminates (normally or abnormally) runs every reached but currently unexecuted defer 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.

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 statements

If 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 _Noreturn function specifier, or a function annotated with the no_return/_Noreturn attribute, is called;

  • or, any signal SIGABRT, SIGINT, or SIGTERM occurs;

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 longjmp function is invoked successfully, or 0 otherwise.

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, SIGSEGV, or SIGTERM are raised, respectively, or 0 otherwise.

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 MB_LEN_MAX; and,

__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, exit, or quick_exit are invoked and terminate the program, respectively, or 0 otherwise.

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 MB_LEN_MAX; and,

__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 thrd_exit is invoked and terminates the thread or program, or 0 otherwise.

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.