Fortified System Functions

Updated: April 19, 2023

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.

Building components to use fortified system functions

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:

0 or undefined
Disables fortified system functions.
1
Enables fortified system functions. Programs that call system functions as described in the QNX Neutrino 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 Neutrino documentation, it might fail (e.g., because it supports an unsafe feature).

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.

Required compiler options

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

If you explicitly specify settings, fortified system functions require either:
  • 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 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 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.

Best practices

The following best practices can help you optimize the benefits of using fortified system functions.

Use objects of constant size

Fortified system functions can only perform checks for out-of-bounds memory writes when the size of the destination object is known to be constant at compile time. When practical:
  • 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. 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.

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

Avoid the -fno-builtin compiler option

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.

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.

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.

Note: This will affect all instances where a SIGABRT signal is raised, including those unrelated to fortified functions

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

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

Supported fortified system functions

QNX Neutrino supports the following fortified system functions:

Function

memccpy()
memcpy()
memcpy_isr()
memcpyv()
memmove()
memset()
memset_isr()
memset_s()
mq_open()
mq_receive()
mq_timedreceive()
mq_timedreceive_monotonic()
open()
open64()
openat()
sem_open()
sopen()
stpncpy()
strlcat()
strlcpy()
strncat()
strncpy()

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()
sprintf()
stpcpy()
strcat()
vsnprintf()
vsprintf()
wcscat()
wcscpy()
wcsncat()
wcsncpy()
wmemcpy()
wmemmove()
wmemset()

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()
strcpy_isr()