Fortified System Functions
QNX OS 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 OS provides are designed to prevent the following scenarios:
- A function writes data outside the bounds of a destination object whose length can be determined at compile time.
- A function that accepts a variable number of arguments processes more arguments than the number actually specified by the caller.
- A formatted I/O function writes data to one or more unintended destinations because
there is an
n
conversion specifier (e.g.,%n
) in the format string.
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.
Building components to use fortified system functions
You must rebuild your executables and libraries to take advantage of QNX OS fortified system functions.
To enable or disable fortified system functions, define the _FORTIFY_SOURCE feature test macro with one of the following values:
0
or undefined- Disables fortified system functions.
1
- Enables fortified system functions. Programs that call system functions as described in the QNX OS documentation run normally.
2
- Enables fortified system functions with more stringent parameter validation.
Even if a program's use of system functions follows the QNX OS documentation, it might fail (e.g., because it
supports an unsafe feature).
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");
This setting also disables the
n
conversion specifier for formatted I/O functions (e.g.,%n
).
By default, binaries under target/qnx7/ are built with _FORTIFY_SOURCE enabled at level 2 (with a few exceptions due to incompatibilities).
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.
Required compiler options
When you rebuild your
executables and libraries to take advantage of QNX OS
fortified system functions, make sure that you use compatible compiler optimization
settings. The default compiler optimization settings used by QNX OS are compatible (see information on the
OPTIMIZE_TYPE macro in the The qrules.mk include
file
section in the Programmer's Guide).
- an optimization level of 1 or higher (-O1, -O2, etc.; because a setting of 1 might not detect as many buffer overflow anomalies, QNX recommends 2 or higher), or
- optimization for code size (-Os).
The compiler emits a warning at compile time if the optimization settings are incompatible with _FORTIFY_SOURCE.
The use of fortified system functions is implicitly disabled when compiler optimization is disabled.Enabling the use of fortified system functions for an entire project
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 the use of fortified system functions for specific source files
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)
{
/* ... */
}
Enabling the use of fortified system functions via the command line
A developer, tester, or system integrator can use the CPPOPTS environment variable 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 && CPPOPTS="-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.
Best practices
The following best practices can help you optimize the benefits of using fortified system functions.Use objects of constant size
- Specify integer constant expressions rather than variables as size arguments to memory allocation functions like malloc(), calloc(), and alloc().
- Avoid variable-length arrays (VLAs).
Avoid using msg + 1 to refer to the payload that follows a message header
Consider the following type for a message header: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 (the payload).
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. Instead, you should use the _PAYLOAD_OF() macro to get a pointer to the
memory that follows a specific header member. Don't use the
_DEVCTL_DATA() macro; this is deprecated. 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.
Downgrade individual source files to _FORTIFY_SOURCE level 1 to work around false positives
If compiling your project with _FORTIFY_SOURCE level 2 results in compile-time or runtime failures that you've determined are false positives, consider downgrading only the affected source files to _FORTIFY_SOURCE level 1, rather than the entire project. To downgrade individual source files, include the following preprocessor logic before all #include directives in each file:#if defined(_FORTIFY_SOURCE) && ( _FORTIFY_SOURCE > 1 )
#undef _FORTIFY_SOURCE
#define _FORTIFY_SOURCE 1
#endif
Diagnostic messages
Warning or error messages that the compiler emits as part of the fortified system functions feature have the prefix _FORTIFY_SOURCE:.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. |
format string contains 'n' conversion specifier | A function that performs formatted output (e.g., printf()) was
called with a format string containing the n conversion specifier
(e.g., %n ). |
Limiting the severity of compile-time diagnostic messages to warnings
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.
Customizing runtime behavior
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.
Debugging with fortified system functions
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 |
---|---|
asprintf | __asprintf_chk |
c16rtomb | __c16rtomb_chk |
c32rtomb | __c32rtomb_chk |
dprintf | __dprintf_chk |
fprintf | __fprintf_chk |
fwprintf | __fwprintf_chk |
gets | __gets_chk |
getwd | __getwd_chk |
printf | __printf_chk |
readblock | __readblock_chk |
readdir64_r | __readdir64_r_chk |
readdir_r | __readdir_r_chk |
realpath | __realpath_chk |
searchenv | __searchenv_chk |
snprintf | __snprintf_chk |
sprintf | __sprintf_chk |
stpcpy | __stpcpy_chk, __strcpy_chk, __memcpy_chk |
strcat | __strcat_chk, __strcpy_chk, __memcpy_chk |
strncat | __strncat_chk, __strcat_chk, __strcpy_chk, __memcpy_chk |
swprintf | __swprintf_chk |
SyncCtl | __SyncCtl_chk |
SyncCtl_r | __SyncCtl_r_chk |
syslog | __syslog_chk |
ThreadCtl | __ThreadCtl_chk |
ThreadCtlExt | __ThreadCtlExt_chk |
ThreadCtlExt_r | __ThreadCtlExt_r_chk |
ThreadCtl_r | __ThreadCtl_r_chk |
vasprintf | __vasprintf_chk |
vdprintf | __vdprintf_chk |
vfprintf | __vfprintf_chk |
vfwprintf | __vfwprintf_chk |
vprintf | __vprintf_chk |
vsnprintf | __vsnprintf_chk |
vsprintf | __vsprintf_chk |
vswprintf | __vswprintf_chk |
vsyslog | __vsyslog_chk |
vwprintf | __vwprintf_chk |
wcrtomb | __wcrtomb_chk |
wcscat | __wcscat_chk |
wcscpy | __wcscpy_chk |
wcsncat | __wcsncat_chk |
wctomb | __wctomb_chk |
wprintf | __wprint_chk |
Supported fortified system functions
QNX OS supports the following fortified system functions:
accept() accept4() asprintf() c16rtomb() c32rtomb() confstr() ConnectClientInfo() ConnectClientInfo_r() ConnectClientInfoAble() devctl() devctlv() dprintf() FD_CLR() FD_SET() fgets() fgetws() fprintf() fread() fwprintf() getcwd() getdelim() getdomainname() getgrouplist() getgroups() gethostname() getline() getlogin_r() getpeername() gets() getsockname() getsockopt() getwd() ioctl() iofdinfo() iofunc_client_info_able() lio_listio() lio_listio64() mbsrtowcs() mbstowcs() memccpy() memcpy() memcpyv() memmove() memset() memset_s() mq_clockreceive() mq_open() mq_receive() mq_timedreceive() mq_timedreceive_monotonic() MsgRead() MsgRead_r() MsgReadv() MsgReadv_r() MsgReceive() |
MsgReceive_r() MsgReceivePulse() MsgReceivePulse_r() MsgReceivev() MsgReceivev_r() MsgSend() MsgSend_r() MsgSendnc() MsgSendnc_r() MsgSendsv() MsgSendsv_r() MsgSendsvnc() MsgSendsvnc_r() MsgSendv() MsgSendv_r() MsgSendvnc() MsgSendvnc_r() MsgSendvs() MsgSendvs_r() MsgSendvsnc() MsgSendvsnc_r() open() open64() openat() pathfind_r() poll() posix_devctl() pread() pread64() printf() pselect() ptsname_r() read() readdir_r() _readdir_r() readdir64_r() _readdir64_r() readblock() readcond() readlink() readlinkat() readv() realpath() recv() recvfrom() recvmmsg() recvmsg() resmgr_msgget() resmgr_msggetv() resmgr_msgread() resmgr_msgreadv() resmgr_pathname() SchedCtl() SchedCtl_r() select() |
searchenv() sem_open() sendmmsg() SETIOV() snprintf() sopen() sprintf() stpcpy() stpncpy() straddstr() strcat() strcpy() strftime() strlcat() strlcpy() strncat() strncpy() strxfrm() swab() swprintf() SyncCtl() SyncCtl_r() syslog() ThreadCtl() ThreadCtl_r() ThreadCtlExt() ThreadCtlExt_r() TraceEvent() ttyname_r() vsyslog() vasprintf() vdprintf() vfprintf() vfwprintf() vprintf() vsnprintf() vsprintf() vswprintf() vsyslog() vwprintf() wcrtomb() wcsftime() wcscat() wcscpy() wcsncat() wcsncpy() wcsrtombs() wcstombs() wcsxfrm() wctomb() wmemcpy() wmemmove() wmemset() wprintf() |