This section details recommendations for increasing application robustness, by avoiding potential issues related to dynamic linking. The recommendations have two main aims: reduce the involvement of the dynamic linker in application execution after process startup, and restrict the application to a dynamic linker feature set whose behavior is more easily understood.
Key aspects of limiting dynamic linker usage after startup are: no use
of the dlopen
function, disabling lazy binding, and using the
static TLS model. More easily understood dynamic linker behavior
requires avoiding name conflicts (symbols and sonames) and highly
customizable features like the audit subsystem.
Note that while these steps can be considered a form of application hardening, they do not guard against potential harm from accidental or deliberate loading of untrusted or malicious code. There is only limited overlap with traditional security hardening for applications running on GNU systems.
Avoiding certain dynamic linker features can increase predictability of applications and reduce the risk of running into dynamic linker defects.
dlopen
, dlmopen
, or
dlclose
. Dynamic loading and unloading of shared objects
introduces substantial complications related to symbol and thread-local
storage (TLS) management.
dlopen
function, dlsym
and dlvsym
cannot be used with shared object handles. Minimizing the use of both
functions is recommended. If they have to be used, only the
RTLD_DEFAULT
pseudo-handle should be used.
dlopen
is not
used, there are no compatibility concerns for initial-exec TLS. This
TLS model avoids most of the complexity around TLS access. In
particular, there are no TLS-related run-time memory allocations after
process or thread start.
If shared objects are expected to be used more generally, outside the
hardened, feature-restricted context, lack of compatibility between
dlopen
and initial-exec TLS could be a concern. In that case,
the second-best alternative is to use global-dynamic TLS with GNU2 TLS
descriptors, for targets that fully implement them, including the fast
path for access to TLS variables defined in the initially loaded set of
objects. Like initial-exec TLS, this avoids memory allocations after
thread creation, but only if the dlopen
function is not used.
libc.so.6
) are always loaded.
Specifically, if a main program or shared object references a symbol,
create an ELF DT_NEEDED
dependency on that shared object, or on
another shared object that is documented (or otherwise guaranteed) to
have the required explicit dependency. Referencing a symbol without a
matching link dependency results in underlinking, and underlinked
objects cannot always be loaded correctly: Initialization of objects may
not happen in the required order.
libA.so.1
depending on libB.so.1
depending on libC.so.1
depending on
libA.so.1
). The GNU C Library has to initialize one of the objects in
the cycle first, and the choice of that object is arbitrary and can
change over time. The object which is initialized first (and other
objects involved in the cycle) may not run correctly because not all of
its dependencies have been initialized.
Underlinking (see above) can hide the presence of cycles.
LD_AUDIT
, DT_AUDIT
,
DT_DEPAUDIT
). Its callback and hooking capabilities introduce a
lot of complexity and subtly alter dynamic linker behavior in corner
cases even if the audit module is inactive.
Exceptions to this rule are copy relocations (see the next item), and vague linkage, as used by the C++ implementation (see below).
A different approach to this situation uses hidden visibility for symbols in the static library, but this can cause problems if the library does not expect that multiple copies of its code coexist within the same process, with no or partial sharing of state.
.text
).
DT_PREINIT_ARRAY
dynamic tag, and do not flag
objects as DF_1_INITFIRST
. Do not change the default linker
script of BFD ld. Do not override ABI defaults, such as the dynamic
linker path (with --dynamic-linker).
dlopen
. Use iconv_open
with built-in converters only
(such as UTF-8
). Do not use NSS functionality such as
getaddrinfo
or getpwuid_r
unless the system is configured
for built-in NSS service modules only (see below).
Several considerations apply to ELF constructors and destructors.
DT_NEEDED
dependency on the object that needs to be initialized
earlier.
dlsym
and
dlvsym
, it is still possible to access uninitialized facilities
even with these restrictions in place. (Of course, access to
uninitialized functionality is also possible within a single shared
object or the main executable, without resorting to explicit symbol
lookup.) Consider using dynamic, on-demand initialization instead. To
deal with access after de-initialization, it may be necessary to
implement special cases for that scenario, potentially with degraded
functionality.
dlsym
and dlvsym
function calls, for
example if client code using a shared object has registered callbacks or
objects with another shared object. The ELF destructor for the client
code is executed before the ELF destructor for the shared objects that
it uses, based on the expected dependency order.
dlopen
and dlmopen
are not used, DT_NEEDED
dependency information is complete, and lazy binding is disabled, the
execution order of ELF destructors is expected to be the reverse of the
ELF constructor order. However, two separate dependency sort operations
still occur. Even though the listed preconditions should ensure that
both sorts produce the same ordering, it is recommended not to depend on
the destructor order being the reverse of the constructor order.
The following items provide C++-specific guidance for preparing applications. If another programming language is used and it uses these toolchain features targeted at C++ to implement some language constructs, these restrictions and recommendations still apply in analogous ways.
By default, variables of block scope of static storage have consistent addresses across different translation units, even if defined in functions that use vague linkage.
Due to the complex interaction between ELF symbol management and C++ symbol generation, it is recommended to use C++ language features for symbol management, in particular inline namespaces.
This does not matter if the original (language-independent) advice regarding symbol interposition is followed. However, as the advice may be difficult to implement for C++ applications, it is recommended to avoid ODR violations across the entire process image. Inline namespaces can be helpful in this context because they can be used to create distinct ELF symbols while maintaining source code compatibility at the C++ level.
STB_GNU_UNIQUE
binding type do not follow the usual ELF symbol
namespace isolation rules: such symbols bind across RTLD_LOCAL
boundaries. Furthermore, symbol versioning is ignored for such symbols;
they are bound by symbol name only. All their definitions and uses must
therefore be compatible. Hidden visibility still prevents the creation
of STB_GNU_UNIQUE
symbols and can achieve isolation of
incompatible definitions.
RTLD_LOCAL
or dlmopen
.
This can cause issues in applications that contain multiple incompatible definitions of the same type. Inline namespaces can be used to create distinct symbols at the ELF layer, avoiding this type of issue.
dlmopen
namespaces may
not work, particular with the unwinder in GCC versions before 12.
Current toolchain versions are able to process unwinding tables across
dlmopen
boundaries. However, note that type comparison is
name-based, not address-based (see the previous item), so exception
types may still be matched in unexpected ways. An important special
case of exception handling, invoking destructors for variables of block
scope, is not impacted by this RTTI type-sharing. Likewise, regular
virtual member function dispatch for objects is unaffected (but still
requires that the type definitions match in all directly involved
translation units).
Once more, inline namespaces can be used to create distinct ELF symbols for different types.
dlclose
.
thread_local
variables with thread
storage duration of types that have non-trivial destructors. However,
in this case, memory allocation failure during registration leads to
process termination. If process termination is not acceptable, use
thread_local
variables with trivial destructors only.
Functions for per-thread cleanup can be registered using
pthread_key_create
(globally for all threads) and activated
using pthread_setspecific
(on each thread). Note that a
pthread_key_create
call may still fail (and
pthread_create
keys are a limited resource in the GNU C Library), but
this failure can be handled without terminating the process.
This subsection recommends tools and build flags for producing applications that meet the recommendations of the previous subsection.
bfd.ld
) from GNU binutils to produce binaries,
invoked through a compiler driver such as gcc
. The version
should be not too far ahead of what was current when the version of
the GNU C Library was first released.
.so
files (which can be linker
scripts) and searching with the -l option. Do not specify the
file names of shared objects on the linker command line.
LOAD
segment in the ELF program header, file
offsets, memory sizes, and load addresses are multiples of the largest
page size supported at run time. Similarly, the start address and size
of the GNU_RELRO
range should be multiples of the page size.
Avoid creating gaps between LOAD
segments. The difference
between the load addresses of two subsequent LOAD
segments should
be the size of the first LOAD
segment. (This may require linking
with -Wl,-z,noseparate-code.)
This may not be possible to achieve with the currently available link editors.
GNU_RELRO
region
cannot be achieved, ensure that the process memory image right before
the start of the region does not contain executable or writable memory.
In some cases, if the previous recommendations are not followed, this can be determined from the produced binaries. This section contains suggestions for verifying aspects of these binaries.
NEEDED
entries are present. (It is not necessary to
list indirect dependencies if these dependencies are guaranteed to
remain during the evolution of the explicitly listed direct
dependencies.)
NEEDED
entries should not contain full path names including
slashes, only sonames
.
NEEDED
entries in dynamic segments, transitively, starting at
the main program. Then determine their dynamic symbol tables (using
‘readelf -sDW’, for example). Ideally, every symbol should be
defined at most once, so that symbol interposition does not happen.
If there are interposed data symbols, check if the single interposing definition is in the main program. In this case, there must be a copy relocation for it. (This only applies to targets with copy relocations.)
Function symbols should only be interposed in C++ applications, to implement vague linkage. (See the discussion in the C++ recommendations above.)
NEEDED
entries, check that the
dependency graph does not contain any cycles.
BIND_NOW
on the
FLAGS
line or NOW
on the FLAGS_1
line (one is
enough).
R_AARCH64_TLS_TPREL
and
X86_64_TPOFF64
. As the second-best option, and only if
compatibility with non-hardened applications using dlopen
is
needed, GNU2 TLS descriptor relocations can be used (for example,
R_AARCH64_TLSDESC
or R_X86_64_TLSDESC
).
__tls_get_addr
, __tls_get_offset
,
__tls_get_addr_opt
in the dynamic symbol table (in the
‘readelf -sDW’ output). Supporting global dynamic TLS relocations
(such as R_AARCH64_TLS_DTPMOD
, R_AARCH64_TLS_DTPREL
,
R_X86_64_DTPMOD64
, R_X86_64_DTPOFF64
) should not be used,
either.
dlopen
, dlmopen
, dlclose
should not be referenced from the dynamic symbol table.
SONAME
entry that matches
the file name (the base name, i.e., the part after the slash). The
SONAME
string must not contain a slash ‘/’.
RPATH
or RUNPATH
entries.
AUDIT
,
DEPAUDIT
, AUXILIARY
, FILTER
, or
PREINIT_ARRAY
tags.
HASH
tag, it
must also contain a GNU_HASH
tag.
INITFIRST
flag (undeer FLAGS_1
) should not be used.
LOAD
segments that are writable
and executable at the same time.
GNU_STACK
program header that
is not marked as executable. (However, on some newer targets, a
non-executable stack is the default, so the GNU_STACK
program
header is not required.)
In addition to preparing program binaries in a recommended fashion, the run-time environment should be set up in such a way that problematic dynamic linker features are not used.
LD_…
variables such as
LD_PRELOAD
or LD_LIBRARY_PATH
, or GLIBC_TUNABLES
)
to change default dynamic linker behavior.
ldconfig
,
usually /etc/ld.so.conf, or in files included from there.)
glibc-hwcaps
subdirectories.
dlopen
facility. The files
and dns
modules are built in and do not rely on dlopen
.
rename
to replace the
already-installed version.