[PATCH v2 1/1] manual: document audit interface
DJ Delorie
dj@redhat.com
Thu Sep 24 01:01:35 GMT 2026
This is my attempt at initial documentation for the la_* audit
interface, based on reading the code...
---
Changes in v2:
- fixed typos.
- elaborated on what a cookie is
- elaborated on what "isolated" means for auditors
- replaced "cookie" with "private data" and rewrote the intro for it
- various clarifications based on review feedback.
- mention that auditors are not "I can do anything" hooks
- Explain DT_DEPAUDIT better
- explain how la_version checks work
- mention performance issues with PLT auditing
- mention non-mentioning of decoding arguments
manual/dynlink.texi | 332 ++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 332 insertions(+)
diff --git a/manual/dynlink.texi b/manual/dynlink.texi
index ad4da753a5..1d9dcd8cdb 100644
--- a/manual/dynlink.texi
+++ b/manual/dynlink.texi
@@ -17,6 +17,7 @@ Dynamic linkers are sometimes called @dfn{dynamic loaders}.
* Dynamic Linker Environment Variables:: Environment variables that control the
dynamic linker.
* Dynamic Linker Introspection:: Interfaces for querying mapping
information.
+* Dynamic Linker Auditing:: Auditing interface.
* Indirect Functions:: Selecting function implementations at
load time.
* Dynamic Linker Hardening:: Avoiding unexpected issues with
dynamic linking.
@end menu
@@ -736,6 +737,337 @@ A bit mask used to signal that the object
contains SFrame data. See
@end table
+@node Dynamic Linker Auditing
+@section Dynamic Linker Auditing
+@cindex Auditing
+@cindex LD_AUDIT
+
+The dynamic linker's auditing interface allows external code to
+observe and affect the loading of dynamic objects. As this allows the
+external code to modify the operation of the program, great care
+should be taken when creating and deploying audit modules (auditors).
+
+Audit modules should include @code{<link.h>} to import constants and
+prototypes used by the audit API.
+
+Note that while auditors can do pretty much anything to the audited
+program, anything other than the monitoring documented here is not
+supported.
+
+@menu
+* Audit Module Loading:: How modules are selected and loaded.
+* Audit API Callbacks:: Audit functions called by the dynamic linker.
+@end menu
+
+@node Audit Module Loading
+@subsection Audit Module Loading
+
+An audit module (auditor) is a shared object (plus its DT_NEEDED
+dependencies) that is loaded very early in the dynamic loading
+process, and isolated (in that it is in a different link namespace)
+from the main application and other auditors. Once loaded, the
+dynamic loader will call functions in the auditor at key points in the
+load process. Auditors are specified by four methods, each of which
+specifies a colon-separated list of auditors:
+
+@table @code
+
+@item --audit
+If the dynamic linker is invoked manually, it allows an @code{--audit}
+command line parameter followed by a list of auditors.
+
+@item LD_AUDIT
+The @code{LD_AUDIT} environment variable
+
+@item DT_AUDIT
+If a shared object has a DT_AUDIT dynamic tag, that tag specifies a
+list of auditors.
+
+@item DT_DEPAUDIT
+Similarly for the DT_DEPAUDIT dynamic tag. This is an advisory tag
+that hints that auditors are requested not by the object, but by one
+of the shared objects it links against.
+
+@end table
+
+The above methods are listed in order of precedence; auditors loaded
+by @code{--audit} will be loaded first, etc. For each callback the
+dynamic loader calls, the modules will be called in the order in which
+they were loaded.
+
+Note that, other than the environment variable, these may occur
+multiple times. The limit on the number of occurrences is currently
+16. The limit on the number of auditors is also 16.
+
+As each auditor is loaded, it's @code{la_version} callback is called
+to ensure the module supports the API version the dynamic loader is
+using. Auditors which do not are unloaded. Once an auditor is
+loaded, it may audit the loading of further auditors@footnote{The
+current implementation sends events for the searching and loading, but
+nothing significant after that. This may be intentional or it may be
+a bug, and auditors should assume auditing auditors is unsupported but
+plan for such events.}. Each auditor is loaded into its own namespace,
+that is, any symbols exported by the audit will not be visible to the
+main program or other auditors.
+
+If the program running is AT_SECURE (for example, set-user-id
+programs), auditors are limited to secure auditors - auditor names
+must not include @samp{/} characters and will only be searched for in
+secure directories. Additionally, such auditors must themselves
+be set-user-id, and have names less than 255 characters long.
+
+@node Audit API Callbacks
+@subsection Audit API Callbacks
+
+Each auditor provides one or more callbacks in the form of global
+functions that the dynamic linker may call. @code{la_version} is
+required of all auditors, but every other callback is optional. The
+auditor defines the callback if it wants those events.
+
+Each auditor may attach private data to each shared object in the
+system via a word stored the shared object (as an intptr_t, so type
+casting is always needed when storing pointers). There is one word of
+private data per shared object, per auditor.
+
+This word may be an index into an array, a bitmask, or a pointer, and
+the address of it is passed to most callbacks. This word is
+initialized just before calling @code{la_objopen}, to be a pointer to
+the link map for the shared object. The auditor may choose to
+initialize it to something else (typically in @code{la_objopen()}, as
+this is called right after initializing it) in order to store more or
+different data per object. There is no guaranteed way to free or
+clean up allocated data used for this purpose, but a reasonable option
+is to clean it up in @code{la_objclose()} @emph{and} set the word to
+zero at that time. With that, all callbacks would need to check for a
+zero word and assume the related object has been or is being closed.
+
+@deftypefun {unsigned int} la_version (unsigned int version)
+This mandatory callback is called when the auditor is loaded. It is
+passed the API version the dynamic linker is using. It should return
+the API version the module is using, or zero to unload the module. If
+the module and the dynamic linker are using incompatible API versions,
+the auditor is unloaded. The expectation is that the module will
+return the @code{LAV_CURRENT} value from @samp{<link.h>}.
+
+Note that @code{la_version()} may return values other than LAV_CURRENT
+if it has knowledge of what the various versions provide and can adapt
+to the @var{version} given, but doing this is outside the scope of
+this document. This may be complicated by LAV_CURRENT being different
+for different architectures.
+
+@end deftypefun
+
+@deftypefun void la_activity (uintptr_t *privdata, unsigned int flag)
+This callback is used to signal activity in a namespace.
+@var{privdata} points to the private data for a shared object in that
+namespace. The @var{flag} is one of the following:
+
+@table @code
+
+@item LC_ACT_ADD
+A shared object has been added to the namespace. Further additions
+are not signalled until the object is marked consistent again.
+
+@item LC_ACT_DELETE
+One or more shared object will be removed from the namespace. Further
+deletions will not be signalled until the link map is marked
+consistent again.
+
+@item LC_ACT_CONSISTENT
+All adds or deletes are complete for this namespace. After this
+callback, the namespace is considered consistent, and further
+additions or deletions will cause another activity callback. Note
+that this callback will not happen if the namespace is empty.
+
+@end table
+
+@end deftypefun
+
+@deftypefun {char *} la_objsearch (const char *name, uintptr_t
*privdata, unsigned int flag)
+Called before the dynamic linker tries to locate a shared object to
+load it. @var{name} is the name of the object to be loaded. The
+callback must return @var{name} if it does not want to change the
+loader's behavior. Otherwise, it may return a different string to
+change the object to be searched for or loaded, or NULL to prevent the
+object from being searched for (LA_SER_ORIG) or loaded (other flags).
+
+If the auditor returns an alternate string for this callback, the
+string must remain valid for the duration of the application. There
+is no mechanism for freeing any allocated memory.
+
+The @var{privdata} points to the private data for the object doing the
+search. The @var{flag} is one of the following:
+
+@table @code
+
+@item LA_SER_ORIG
+The dynamic linker is starting a search for @var{name}, usually via a
+call to @code{dlopen()} or a @code{DT_NEEDED} entry. A callback with
+this flag will happen before a callback with any of the other flags,
+which indicate progress through the search.
+
+@item LA_SER_LIBPATH
+The dynamic loader is searching for @var{name} in the paths specified
+by @code{LD_LIBRARY_PATH}, and a candidate object has been found. If
+@var{name} is changed, it must be to an existing file that can be
+opened without searching for it.
+
+@item LA_SER_RUNPATH
+The dynamic loader found a candidate for @var{name} in the paths
+specified by @code{DT_RPATH}.
+
+@item LA_SER_CONFIG
+The dynamic loader found a candidate for @var{name} in the paths
+stored in @file{ld.so.cache}.
+
+@item LA_SER_DEFAULT
+The dynamic loader found a candidate for @var{name} in the default
+paths hardcoded in the dynamic linker itself.
+
+@item LA_SER_SECURE
+Unused in Linux.
+
+@end table
+
+@end deftypefun
+
+@deftypefun {unsigned int} la_objopen (struct link_map *map, Lmid_t
lmid, uintptr_t *privdata)
+Called when a shared object is loaded. Unlike @code{la_activity} this
+callback is called once for each object. @var{map} is the link map
+for the object. The @var{privdata} points to the private data for the
+object being loaded, is initialized to @var{map} before this
+callback@footnote{As if this callback did @code{*privdata =
+(uintptr_t) map}}, and may be initialized to point to auditor-specific
+data by this callback. @var{lmid} is the namespace the object was
+loaded into, either @code{LM_ID_BASE} for the base namespace, or some
+other value for namespaces created with @cdoe{dlmopen()}.
+
+The value returned by this callback is a bitmask of zero or more of
+these flags:
+
+@table @code
+
+@item LA_FLG_BINDTO
+Audit symbols bound to this object.
+
+@item LA_FLG_BINDFROM
+Audit symbols bound from this object.
+
+@end table
+
+@end deftypefun
+
+@deftypefun {unsigned int} la_objclose (uintptr_t *privdata)
+Called when a shared object is unloaded. This is called after
+@code{la_activity} signals its deletion. The return value is ignored.
+
+@end deftypefun
+
+@deftypefun void la_preinit (uintptr_t *privdata)
+This callback is called just before the dynamic linker passes control
+to the application. The @var{privdata} always points to the private
+data for the main application.
+
+@end deftypefun
+
+@deftypefun uintptr_t la_symbind (Elf_Sym *sym, unsigned int ndx,
uintptr_t *refdata, uintptr_t *defdata, unsigned int *flags, const
char *symname)
+@findex la_symbind32
+@findex la_symbind64
+Called when a binding occurs between the use of a symbol @var{sym}
+with name @var{symname} and its definition. @var{refdata} points to
+the private data for the object referencing the symbol, and
+@var{defdefdata} points to the private data for the object providing
+the definition of the symbol. @var{ndx} is the index of the symbol in
+the symbol table of the shared object in which it is defined.
+
+This callback will be called only for symbols in objects where
+@code{la_objopen} returned a nonzero value. It will also only be
+called for symbols that represent a function call, and that use the
+PLT for that call.
+
+This callback is actually one of two callbacks, either
+@code{la_symbind32} or @code{la_symbind64}, with corresponding
+@code{Elf32_Sym} or @code{Elf_Sym64} parameters. Both callbacks
+function the same other than the symbol size. The auditor may define
+either or both, but if both, only one is ever called - the one that
+corresponds to the application's architecture.
+
+@var{flags} points to a bitmask which may include any of the following:
+
+@table @code
+@item LA_SYMB_NOPLTENTER
+@item LA_SYMB_NOPLTEXIT
+The auditor may add these flags to @var{*flags} to request skipping
+callbacks for the architecture-specific la_pltenter and/or la_pltexit
+events. Note that not including these flags may impact performance,
+as certain optimizations (such as replacing a PLT call with a non-PLT
+call) must be disabled for these callbacks, in addition to the
+per-call overhead of the auditor itself.
+
+@item LA_SYMB_STRUCTCALL
+Unused in Linux.
+
+@item LA_SYMB_DLSYM
+The symbol is being bound due to a @code{dlsym()} call.
+
+@item LA_SYMB_ALTVALUE
+The symbol's value has been altered by a previous @var{la_symbind()}
+callback
+
+@end table
+
+The value returned by @code{la_symbind()} is used as the address of
+the symbol. The auditor should return @code{sym->st_value} if it does
+not wish to modify the application's behavior.
+
+@end deftypefun
+
+@deftypefun uintptr_t la_pltenter (@dots{})
+This callback is one of many architecture-defined callbacks listed in
+@code{<link.h>}, refer to that for specifics. The parameters are
+similar to @code{la_symbind}, with the addition of one or more
+parameters for passing register contents. For example, the x86-64
+prototype is:
+
+@example
+Elf64_Addr la_x86_64_gnu_pltenter (Elf64_Sym *sym,
+ unsigned int ndx, uintptr_t *refdata, uintptr_t *defdata,
+ La_x86_64_regs *regs, unsigned int *flags,
+ const char *symname, long int *framesizep);
+@end example
+
+Note that decoding the arguments is very dependent on the platform and
+compiler flags, and is outside the scope of this document.
+
+@c FIXME add architecture-specific prototpes and documentation
+
+The @code{la_pltenter} callback is called when a caller is about to
+call a function through a PLT entry which is lazily bound, if the
+caller and/or callee objects have been marked for auditing by
+@code{la_objopen()}. The callback returns the address to actually
+call, or @code{sym->st_value} if it does not wish to modify the call.
+
+Note that hardening an executable by disabling lazy binding
+(i.e. using @code{--enable-bind-now} or @code{-z now} when building
+it) may block the ability to trace calls via the PLT.
+
+The callback may modify the frame size used for @code{la_pltexit} via
+the @var{framesizep} pointer. The call will use the largest requested
+frame size. If no auditors modify this, @code{la_pltexit()} will not
+be called.
+
+@end deftypefun
+
+@deftypefun void la_pltexit (@dots{})
+Architecture-specific function similar to @code{la_pltenter}. This
+callback is called when a function, called through the PLT, returns to
+the caller. It will only be called if @code{la_pltenter} specified a
+frame size for it. @var{inregs} contains the original registers at
+the time of @code{la_pltenter}, and @var{outregs} contains the return
+value of the function, which the auditor may modify.
+
+@end deftypefun
+
@node Indirect Functions
@section Indirect Functions
@cindex indirect function
--
2.55.0
More information about the Libc-alpha
mailing list