Updated: October 28, 2024 |
QNX Neutrino RTOS fortified system functions are designed to detect out-of-bounds memory accesses by performing lightweight parameter validation at compile time, runtime, or both.
An out-of-bounds memory write vulnerability may allow an attacker to modify program behavior or execute arbitrary instructions.
The fortified system functions that QNX Neutrino provides are designed to prevent the following scenarios:
Because these anomalies can corrupt the execution state of the program, it may not be possible to successfully apply a mitigation if they are detected after the fact. For this reason, the fortified system functions terminate the process rather than allow the overflow or other anomaly to occur.
When parameter validation fails at compile-time, the compiler emits a warning or error message.
When parameter validation fails at runtime, diagnostic information is output to the controlling terminal and the process is terminated.
A fortified system function is only able to catch a potential out-of-bounds memory write when the compiler is able to determine the length of the destination object passed to that function. When the compiler is unable to determine the length of the destination object, the fortified system function behaves exactly as the regular version of that system function would.
However, unlike development tools that detect out-of-range memory accesses more exhaustively (e.g., AddressSanitizer (ASan), Valgrind Memcheck), fortified system functions add negligible performance overhead. For this reason, QNX recommends using this feature on both development and production systems.
You must rebuild your executables and libraries to take advantage of QNX Neutrino fortified system functions.
To enable or disable fortified system functions, define the _FORTIFY_SOURCE feature test macro with one of the following values:
Currently, using this value makes parameter validation more stringent for some functions, mostly the string-related ones. To determine if an overflow will occur, the bounds of individual structure members are evaluated, not just the bounds of the objects they are a component of.
For example, the compiler does not emit an error when the following code is compiled with a _FORTIFY_SOURCE setting of 1, but it does emit one when _FORTIFY_SOURCE is 2:
struct { char key[8]; int value; } key_value_pair; strcpy(key_value_pair.key, "too long");
You can use the qchecksec utility to determine both whether -D_FORTIFY_SOURCE=[1|2] was specified when the source was compiled. See the qchecksec entry in the Utilities Reference.
When you rebuild your executables and libraries to take advantage of QNX Neutrino fortified system functions, make sure that you use compatible compiler optimization settings. The default compiler optimization settings used by QNX Neutrino are compatible (see information on the OPTIMIZE_TYPE macro in the The qrules.mk include file section in the Programmer's Guide).
Enabling fortified system functions in makefiles via the CPPFLAGS variable is a convenient way for a developer to selectively enable or disable the feature for all modules. For example:
CPPFLAGS += -D_FORTIFY_SOURCE=2
Enabling fortified system functions in source code allows a developer to selectively enable or disable the feature on a module-by-module basis. For example:
#if defined(_FORTIFY_SOURCE) && ( _FORTIFY_SOURCE < 2 ) #undef _FORTIFY_SOURCE #endif #ifndef _FORTIFY_SOURCE #define _FORTIFY_SOURCE 2 #endif #include <stdio.h> #include <string.h> int main(int argc, char **argv) { /* ... */ }
A developer, tester, or system integrator can use the CCOPTS and CXXOPTS environment variables to rebuild an existing application or library with a different _FORTIFY_SOURCE setting without having to modify any source files or makefiles. For example:
make clean && CCOPTS="-D_FORTIFY_SOURCE=2" CXXOPTS="-D_FORTIFY_SOURCE=2" make
In some cases, -U_FORTIFY_SOURCE is required before -D_FORTIFY_SOURCE=2 to override a _FORTIFY_SOURCE setting that is specified in one or more makefiles.
struct msg_s { uint16_t type; /* Message type */ uint16_t length; /* Number of bytes of data that immediately follow */ };If the pointer to an instance of that message header type is msg, it is convenient to use the idiom msg + 1 to refer to the memory that immediately follows the header. Unfortunately, because fortified system functions frequently can't distinguish this well-intentioned pointer expression from an anomalous out-of-bounds array access, this expression can lead to false-positive failures.
To avoid this problem, whenever possible, define message types that represent a variable-length payload using a flexible array member and use that member to refer to the payload. For example, to modify the example message header type to use a flexible array member:
struct msg_s { uint16_t type; /* Message type */ uint16_t length; /* Number of bytes of data that immediately follow */ char data[]; /* Payload */ };
Use the new member to refer to the payload (e.g., msg->data) instead of the expression msg + 1.
#if defined(_FORTIFY_SOURCE) && ( _FORTIFY_SOURCE > 1 ) #undef _FORTIFY_SOURCE #define _FORTIFY_SOURCE 1 #endif
The compiler's built-in implementations of memory allocation functions (malloc(), etc.) track the sizes of allocated objects and make that information available to fortified system functions. The -fno-builtin compiler option disables these built-in implementations, which prevents fortified system functions from performing bounds checks on allocated objects.
In some cases, some additional interpretation is required to find the source of an error generated by fortified system functions.
When fortified system functions are enabled and client source code makes a call to a fortified system function, the system instead calls a function that has the same signature as the original, fortified function (i.e., same return value, same number of parameters, and same parameter types). This inline wrapper function performs additional validation of the function's parameters and its location is used by any errors it generates. However, the actual source of the error is in the client code that called the original system function.
For example, the source file /home/jdoe/foo/bar.c contains the following code:
#include <limits.h> #include <stdio.h> #include <string.h> int main(int argc, char **argv) { char buf[16] = ""; if ( argc > 1 ) { strlcpy(buf, argv[1], PATH_MAX); } printf("buf == '%s'\n", buf); return 0; }
This code generates the following error, which indicates that the source of the error is the call to strlcpy():
In file included from /home/jdoe/qnx/sdp/target/qnx7/usr/include/string.h:176, from /home/jdoe/foo/bar.c:3: In function 'strlcpy', inlined from 'main' at /home/jdoe/foo/bar.c:10:9: /home/jdoe/qnx/sdp/target/qnx7/usr/include/string_chk.h:308:13: error: call to '__fortify_fail_overflow_dst_diag_strlcpy' declared with attribute error: _FORTIFY_SOURCE: argument 3 of 'strlcpy' is greater than the length of the object referenced by argument 1 __fortify_fail_overflow_dst_diag_strlcpy(); ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
When the fortified system functions feature terminates a process at runtime, it outputs a diagnostic message to the process's controlling terminal. This diagnostic message includes a brief description of the anomaly (e.g., buffer overflow) and states that the process has been terminated.
Description in message | Description of anomaly |
---|---|
destination buffer overflow | Data to write would overflow the destination object. OR An index or size argument specifies a location that's outside the boundaries of a destination object. |
function called with insufficient arguments | A function was called with fewer arguments than is expected given the value or values of one or more non-optional arguments. |
function called with too many arguments | A function was called with a greater number of arguments than is ever considered valid for that function. |
To reduce the effort required to enable the _FORTIFY_SOURCE feature on an existing project, define the _FORTIFY_SOURCE_WARNINGS_ONLY feature test macro with a value of 1. When operating in this mode and parameter validation fails for a call to a fortified system function, the compiler emits a warning instead of an error. When the resulting binary is executed, the process terminates at that same fortified system function call. This behavior allows you to get a complete list of the issues to fix without having to rebuild your project multiple times.
You can use the same methods you use to define _FORTIFY_SOURCE to define _FORTIFY_SOURCE_WARNINGS_ONLY (i.e., source code, makefiles, command line).
When you enable the _FORTIFY_SOURCE_WARNINGS_ONLY feature, make sure you specify the -Wsystem-headers compiler option. Otherwise, the compiler may mask some of the warnings that are emitted as errors when _FORTIFY_SOURCE_WARNINGS_ONLY is not enabled.
Runtime diagnostic messages outputted by fortified functions in response to a failed parameter validation can be output to standard error by setting the LIBC_FATAL_STDERR to a non-empty value. By default, the messages will be written to /dev/tty. Refer to Commonly Used Environmental Variables for more information.
To customize the behavior of fortified functions after outputting a diagnostic message, use signal() to register a SIGABRT signal handler.
If you look at disassembled binaries for systems where fortified system functions are enabled (e.g., as part of your debugging process), you may see instances where a variant of a system function is called where your source code calls a system function. These variants perform parameter validation at runtime and terminate the process when validation fails. When you enable the use of fortified system functions, the compiler replaces some or all of the calls to the following system functions with calls to the corresponding variant:
Function | Variant |
---|---|
snprintf |
__snprintf_chk |
sprintf |
__sprintf_chk |
stpcpy |
__stpcpy_chk |
strcat |
__strcat_chk |
strncat |
__strncat_chk |
vsnprintf |
__vsnprintf_chk |
vsprintf |
__vsprintf_chk |
wcscat |
__wcscat_chk |
wcscpy |
__wcscpy_chk |
wcsncat |
__wcsncat_chk |
wcsncpy |
__wcsncpy_chk |
wmemcpy |
__wmemcpy_chk |
wmemmove | __wmemmove_chk |
wmemset | __wmemset_chk |
QNX Neutrino supports the following fortified system functions:
Function |
---|
memccpy() |
For the following supported fortified system functions, a warning is emitted when a problem is detected at compile time instead of an error that includes the _FORTIFY_SOURCE: prefix. They will be updated in a future release:
Function |
---|
snprintf() |
For the following supported fortified system functions, buffer overflows are detected and prevented at runtime but not at compile time. They will be updated in a future release:
Function |
---|
strcpy() |