CertC-SIG31¶
Do not access shared objects in signal handlers
Required inputs: IR
Accessing or modifying shared objects in signal handlers can result in race
conditions that can leave data in an inconsistent state. The two exceptions (C
Standard, 5.1.2.3, paragraph 5) to this rule are the ability to read from and
write to lock-free atomic objects and variables of type
volatile sig_atomic_t. Accessing any other type of object from a
signal handler is
undefined
behavior. (See
undefined
behavior 131.)
The need for the
volatile keyword is described in
DCL22-C.
Use volatile for data that cannot be cached.
The type
sig_atomic_t is the
integer type of an object that can be accessed as an atomic entity even in the
presence of asynchronous interrupts. The type of
sig_atomic_t is
implementation-defined,
though it provides some guarantees. Integer values ranging from
SIG_ATOMIC_MIN
through
SIG_ATOMIC_MAX,
inclusive, may be safely stored to a variable of the type. In addition, when
sig_atomic_t is a
signed integer type,
SIG_ATOMIC_MIN must
be no greater than
-127 and
SIG_ATOMIC_MAX no
less than
127. Otherwise,
SIG_ATOMIC_MIN must
be
0 and
SIG_ATOMIC_MAX must
be no less than
255. The macros
SIG_ATOMIC_MIN and
SIG_ATOMIC_MAX are
defined in the header
<stdint.h>.
According to the C99 Rationale [ C99 Rationale 2003], other than calling a limited, prescribed set of library functions,
the C89 Committee concluded that about the only thing a strictly conforming program can do in a signal handler is to assign a value to a
volatile staticvariable which can be written uninterruptedly and promptly return.
However, this issue was discussed at the April 2008 meeting of ISO/IEC WG14,
and it was agreed that there are no known
implementations
in which it would be an error to read a value from a
volatile sig_atomic_t variable, and the original intent of the
committee was that both reading and writing variables of
volatile sig_atomic_t would be strictly conforming.
The signal handler may also call a handful of functions, including
abort(). (See
SIG30-C.
Call only asynchronous-safe functions within signal handlers for more
information.)
Noncompliant Code Example
In this noncompliant code example,
err_msg is updated to indicate that the
SIGINT signal was delivered. The
err_msg variable is a character pointer and not a variable of type
volatile sig_atomic_t.
#include <signal.h>
#include <stdlib.h>
#include <string.h>
enum { MAX_MSG_SIZE = 24 };
char *err_msg;
void handler(int signum) {
strcpy(err_msg, "SIGINT encountered.");
}
int main(void) {
signal(SIGINT, handler);
err_msg = (char *)malloc(MAX_MSG_SIZE);
if (err_msg == NULL) {
/* Handle error */
}
strcpy(err_msg, "No errors yet.");
/* Main code loop */
return 0;
}
Compliant Solution (Writing
volatile sig_atomic_t)
For maximum portability, signal handlers should only unconditionally set a
variable of type
volatile sig_atomic_t and return, as in this compliant solution:
#include <signal.h>
#include <stdlib.h>
#include <string.h>
enum { MAX_MSG_SIZE = 24 };
volatile sig_atomic_t e_flag = 0;
void handler(int signum) {
e_flag = 1;
}
int main(void) {
char *err_msg = (char *)malloc(MAX_MSG_SIZE);
if (err_msg == NULL) {
/* Handle error */
}
signal(SIGINT, handler);
strcpy(err_msg, "No errors yet.");
/* Main code loop */
if (e_flag) {
strcpy(err_msg, "SIGINT received.");
}
return 0;
}
Compliant Solution (Lock-Free Atomic Access)
Signal handlers can refer to objects with static or thread storage durations that are lock-free atomic objects, as in this compliant solution:
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <stdatomic.h>
#ifdef __STDC_NO_ATOMICS__
#error "Atomics are not supported"
#elif ATOMIC_INT_LOCK_FREE == 0
#error "int is never lock-free"
#endif
atomic_int e_flag = ATOMIC_VAR_INIT(0);
void handler(int signum) {
e_flag = 1;
}
int main(void) {
enum { MAX_MSG_SIZE = 24 };
char err_msg[MAX_MSG_SIZE];
#if ATOMIC_INT_LOCK_FREE == 1
if (!atomic_is_lock_free(&e_flag)) {
return EXIT_FAILURE;
}
#endif
if (signal(SIGINT, handler) == SIG_ERR) {
return EXIT_FAILURE;
}
strcpy(err_msg, "No errors yet.");
/* Main code loop */
if (e_flag) {
strcpy(err_msg, "SIGINT received.");
}
return EXIT_SUCCESS;
}
Exceptions
SIG31-C-EX1: The C Standard, 7.14.1.1 paragraph 5 [
ISO/IEC
9899:2011], makes a special exception for
errno when a valid call to the
signal() function results in a
SIG_ERR return, allowing
errno to take an indeterminate value. (See
ERR32-C.
Do not rely on indeterminate values of errno.)
Risk Assessment
Accessing or modifying shared objects in signal handlers can result in accessing data in an inconsistent state. Michal Zalewski's paper "Delivering Signals for Fun and Profit" [ Zalewski 2001] provides some examples of vulnerabilities that can result from violating this and other signal-handling rules.
| Rule | Severity | Likelihood | Remediation Cost | Priority | Level |
|---|---|---|---|---|---|
| SIG31-C | High | Likely | High | P9 | L2 |
Related Guidelines
| Taxonomy | Taxonomy item | Relationship |
|---|---|---|
| ISO/IEC TS 17961:2013 | Accessing shared objects in signal handlers [accsig] | Prior to 2018-01-12: CERT: Unspecified Relationship |
| CWE 2.11 | CWE-662, Improper Synchronization | 2017-07-10: CERT: Rule subset of CWE |
| CWE 2.11 | CWE-828, Signal Handler with Functionality that is not Asynchronous-Safe |
2017-10-30:MITRE:Unspecified Relationship 2018-10-19:CERT:Rule subset of CWE |
Bibliography
| [ C99 Rationale 2003] | 5.2.3, "Signals and Interrupts" |
| [ ISO/IEC 9899:2011] | Subclause 7.14.1.1, "The
signal Function"
|
| [ Zalewski 2001] |
Possible Messages
Key |
Text |
Severity |
Disabled |
|---|---|---|---|
multiple_lock_add |
Lock is acquired while it is already locked. |
None |
False |
non_recursive_lock_add |
Non-recursive lock is acquired while it is already locked. |
None |
False |
removed_nonexisting_lock |
Lock is released, although it is not currently locked. |
None |
False |
unbalanced_locks_path |
Different control flow paths have different sets of locks. |
None |
False |
unbalanced_locks_routine |
Routine may return with different lock set than it is entered with ({in_set} vs {out_set}). |
None |
False |
Options¶
This rule shares the following common options: exclude_in_macros, exclude_messages_in_system_headers, excludes, extend_exclude_to_macro_invocations, includes, justification_checker, languages, post_processing, provider, report_at, severity
The following places define options that affect this rule: Stylechecks, Analysis-GlobalOptions
access_kinds¶
access_kinds : set[bauhaus.ir.LIR_Class_Name] = {'Reading_Operand_Interface', 'Writing_Operand_Interface'}
Reading_Operand_Interface,
Writing_Operand_Interface, Address_Operand_Interface).
allow_c11_atomics¶
allow_c11_atomics : bool = True
allow_volatile_sig_atomic_t¶
allow_volatile_sig_atomic_t : bool = True
volatile sig_atomic_t.
debug_output¶
debug_output : bool = False
enter_critical_functions¶
enter_critical_functions
Set of function names to enter a critical region.Type: set[bauhaus.analysis.config.QualifiedName]
Default:
{'EnterCriticalSection', 'mtx_lock', 'pthread_mutex_lock', 'std::_Mutex_base::lock', 'std::mutex::lock'}
enter_critical_macros¶
enter_critical_macros : set[bauhaus.analysis.config.MacroName] = set()
excluded_routines¶
excluded_routines : set[bauhaus.analysis.config.QualifiedName] = set()
excluded_subgraphs¶
excluded_subgraphs : set[bauhaus.analysis.config.QualifiedName] = set()
exit_critical_functions¶
exit_critical_functions
Set of function names to exit a critical region.Type: set[bauhaus.analysis.config.QualifiedName]
Default:
{'ExitCriticalSection', 'mtx_unlock', 'pthread_mutex_unlock', 'std::_Mutex_base::unlock', 'std::mutex::unlock'}
exit_critical_macros¶
exit_critical_macros : set[bauhaus.analysis.config.MacroName] = set()
inspect_pointers¶
inspect_pointers : bool = False
nested_critical_regions¶
nested_critical_regions : bool = True
output_safe_accesses¶
output_safe_accesses : bool = False
partitions¶
partitions
Dict with partition name as key and dict as value. Partitions describe parts of the IR graph that can be run as a task or an interrupt service routine. The partition dict can contain keys as follows:Type: dict[str, dict[str, typing.Any]]
Default:
{ 'Signal Handlers': { 'functions_passed_to': ['signal', 'sigaction'] }, 'main': { 'entries': ['main'] } }
entries: list of entry functions or this task/isrfunctions_passed_to: name of thread creation function. Any function designated by a pointer passed to that function will be considered an entry function.vectors: list of global variable names with function pointers to entry functions or this task/ISRguarded: boolean property. Set toTrueif this task is nonpreemptive and cannot be interrupted by interrupt handlers. Set toFalseor omit otherwise (default).
__interrupts__ will automatically contain
all interrupt handlers recorded as Additional_Entries in IR (see
compiler toolchain's advanced.main_entries configuration) in addition
to any entries specified in its dict.
report_cfg_based_critical_region_issues¶
report_cfg_based_critical_region_issues : bool = False
show_identical_access¶
show_identical_access : bool = False
show_object_number¶
show_object_number : bool = False
strict_priorities¶
strict_priorities : bool = False
treat_types_as_atomic¶
treat_types_as_atomic : set[typing.Pattern[str] | typing.Tuple[typing.Optional[int], typing.Optional[int], typing.Optional[typing.Pattern[str]]]] = set()