[v2,1/1] manual: document audit interface
Checks
Commit Message
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(+)
* 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
Comments
On 23/09/26 22:01, DJ Delorie wrote:
> This is my attempt at initial documentation for the la_* audit
> interface, based on reading the code...
HI DJ,
Thanks for working on this, it is missing spot from our documentation that
surprises the took so long to be noticed.
The patch seems to have been line-wrapped by the mailer (the menu context
line and the la_objsearch, la_objopen, and la_symbind @deftypefun lines),
so it does not apply with git am and/or git-pw.
> ---
>
> 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.
I think it would be worth to add that it is a GNU extensions and thus
requires _GNU_SOURCE (even though technically this as originally from
Solaris).
> +
> +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:
It might be worth adding that this means each auditor also gets its
own copy of libc (separate malloc heap, stdio, etc.). We had some issue
regarding TLS, so we need to check what is the current support for this.
> +
> +@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.
ld.so only reads DT_AUDIT and DT_DEPAUDIT from the main executable
(dl_main), I think it should be clear that tags from DT_NEEDED are
ignored.
> +
> +@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.
I am not sure if 'advisory' is the correct word here, since ld.so loads
DT_DEPAUDIT exactly like DT_AUDIT. Maybe extend that DT_DEPAUDIT is ignore
if not present.
Also a stale DT_DEPAUDIT *can* become a startup failure when the user is
already close to the auditor limit, since it does count towards the limit.
> +
> +@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.
Afaiu the order is LD_AUDIT, then --audit, then DT_AUDIT, then DT_DEPAUDIT,
and the process_envvars runs before the command-line parsing in dl_main.
With LD_AUDIT=A, --audit B, and DT_AUDIT=C, the la_version calls come in
A, B, C order.
It is also not a precedence, since all of them are loaded. I would list
LD_AUDIT first and say "auditors are loaded in the order listed above".
> +
> +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.
Only --audit can be repeated, the DT tags are read *once* from the
executable. The two limits are also different:
- 16 is the total number of audit strings (LD_AUDIT, each --audit,
DT_AUDIT, and DT_DEPAUDIT combined) and going over it is a fatal
error ("Too many audit modules requested").
- The number of auditors is DL_NNS (16) *minus* glibc.rtld.nns
(default 4), so 12 by default and at most 15 (check
_dl_tls_static_surplus_init). Asking for 13 kills the process at
startup with "Failed loading 13 audit modules, 12 are supported."
Both limits count names in the lists, not modules that loaded
successfully.
> +
> +As each auditor is loaded, it's @code{la_version} callback is called
s/it's/its
> +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.
This could be more concrete: earlier auditors get la_objsearch and
la_objopen for later auditors and their dependencies (with the lmid of
the new auditor's namespace), but no la_activity, la_symbind, or
la_objclose for them.
> +
> +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.
I think it should add that ld.so.cache is not consulted in this case
(_dl_map_object skips it for __RTLD_SECURE), so "only loaded from the
standard search directories" matches the code better.
> +
> +@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.
It is initialized when the link map is created (_dl_new_object), and
the difference is visible. For dlmopen (LM_ID_NEWLM, ...), la_activity
(LA_ACT_ADD) is called with the new object's word before its
la_objopen runs. So callbacks can see the default value (the link map
pointer) before la_objopen has had a chance to change it.
> +
> +@deftypefun {unsigned int} la_version (unsigned int version)
I would add '@standards{GNU, link.h}'. Same for all other functions.
> +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>}.
This could be a bit more precise: returning 0 unloads the module silently
(it is only reported with LD_DEBUG=files), while returning a version
ld.so does not accept prints an error and unloads it. The generic
check accepts any nonzero value up to LAV_CURRENT, but aarch64 and
loongarch require exactly LAV_CURRENT (which we might revise since it
seems a deviation).
> +
> +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.
Should we list the architecture we its differs?
> +
> +@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
s/LC_ACT_ADD/LA_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
s/LC_ACT_ADD/LA_ACT_DELETE
> +One or more shared object will be removed from the namespace. Further
s/object/objects/
> +deletions will not be signalled until the link map is marked
> +consistent again.
> +
> +@item LC_ACT_CONSISTENT
s/LC_ACT_ADD/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).
For the non-ORIG flags, NULL only skips that candidate path, and the
search continues with the next one (open_verify returns -1). With
LD_LIBRARY_PATH=A:B and NULL returned for A's paths, the library is
loaded from B. Also, with more than one auditor, the name returned by
one is passed to the next, and NULL stops the chain.
> +
> +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.
The non-ORIG callbacks happen *before* each open attempt, for every
candidate path, including paths that do not exist (each glibc-hwcaps
subdirectory, for instance). So "is about to try the path @var{name}"
rather than "a candidate has been found"; the same applies to the
other flags. Names that contain a slash only get LA_SER_ORIG.
> +
> +@item LA_SER_RUNPATH
> +The dynamic loader found a candidate for @var{name} in the paths
> +specified by @code{DT_RPATH}.
It is used for DT_RUNPATH as well.
> +
> +@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.
> +
I think it is unused on Hurd as well, same for LA_SYMB_STRUCTCALL.
> +@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()}.
s/@cdoe/@code
Also, it would also be good to mention that la_objopen is called for the
main program, ld.so itself, and the vDSO.
> +
> +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.
Worth adding that it is also called at process exit (from _dl_fini),
which is where most auditors will do their cleanup.
> +
> +@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.
Maybe add that it is called after all ELF constructors, including the
main program's.
> +
> +@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)
Neither la_symbind nor Elf_Sym exists, and the @deftypefun name ends
up in the info dir entries as a "GNU C library function". Maybe:
@deftypefun uintptr_t la_symbind32 (Elf32_Sym *@var{sym}, ...)
@deftypefunx uintptr_t la_symbind64 (Elf64_Sym *@var{sym}, ...)
which also removes the need for the @findex lines.
> +@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
s/defdefdata/defdata/
> +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.
> +
I think it would be better to extend to give more details:
- For relocations it requires LA_FLG_BINDFROM on the referencing
object *and* LA_FLG_BINDTO on the defining object. For dlsym,
either one is enough (_dl_audit_symbind vs.
_dl_audit_symbind_alt).
- It is called for all JUMP_SLOT relocations, lazy or not (BZ #23734)
and it is also called for BIND_NOW objects and LD_BIND_NOW, w
ith LA_SYMB_NOPLTENTER | LA_SYMB_NOPLTEXIT already set in *flags.
For dlsym/dlvsym it is called for any symbol,including data, with
LA_SYMB_DLSYM set. Calls compiled with -fno-plt do not trigger it.
> +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
s/Elf_Sym64/Elf64_Sym/
> +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 second one should be @itemx.
> +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.
Not really, if any auditor defines la_pltenter or la_pltexit, every lazily
bound PLT call goes through _dl_runtime_profile on every call. The
_dl_profile_fixup stores the result in l_reloc_result and never updates the
GOT.
Setting these flags only skips the *callbac*, not the trampoline. The only
way to avoid the overhead is to not define la_pltenter/la_pltexit at all
(_dl_relocate_object only enables profiling when one of them is
present). The ld.so does not replace PLT calls with direct calls.
> +
> +@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()}
Elsewhere the manual writes @code{symbol} without the "()".
> +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{})
The same concern as for la_symbind.
> +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.
I think it would good to make explicit that it requires both (BINDFROM on
the caller and BINDTO on the callee), and it is called on every call through
the PLT, not only the first one.
> +
> +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 -z now does not block it: if an auditor defines la_pltenter or la_pltexit,
_dl_relocate_object ignores DT_BIND_NOW and keeps lazy binding (la_pltenter
is still called for a -z now binary). What disables it is LD_BIND_NOW=1, or
-fno-plt (no PLT at all).
Also --enable-bind-now is glibc's configure option for its own libraries,
not an application build option.
> +
> +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.
It would help to say what the value means: the number of bytes of the
caller's stack (the stack-passed arguments) that the trampoline copies
so it can call the function and regain control for la_pltexit. Any
value >= 0 enables la_pltexit (0 is fine when no arguments are passed
on the stack), and a value that is too small makes the callee read
garbage stack arguments. The default, -1, means no la_pltexit.
> +
> +@end deftypefun
> +
> +@deftypefun void la_pltexit (@dots{})
All the la_*_gnu_pltexit prototypes return unsigned int (the value is
ignored though), check elf/sotruss-lib.c. Also, @var{inregs} and
@var{outregs} below refer to parameters that the (@dots{}) prototype
does not show.
> +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
As a side-note I found two issues (one from my patch) reviewing it, I will send
a fix.
[initial reply only, for now...]
On Fri, Sep 25, 2026 at 3:50 PM Adhemerval Zanella Netto
<adhemerval.zanella@linaro.org> wrote:
> The patch seems to have been line-wrapped by the mailer (the menu context
> line and the la_objsearch, la_objopen, and la_symbind @deftypefun lines),
> so it does not apply with git am and/or git-pw.
Sorry. I'm on a new system and haven't figured out how to send email
via anything other than pasting into a gmail window yet.
> It might be worth adding that this means each auditor also gets its
> own copy of libc (separate malloc heap, stdio, etc.).
I have an alternate intro from Ben that uses "its own libstdc++" for
the example.
> We had some issue regarding TLS, so we need to check what is the
> current support for this.
I submitted a patch for that,it's still pending.
https://inbox.sourceware.org/libc-alpha/xno6fh4ed4.fsf@greed.delorie.com/
> > +@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.
>
> I am not sure if 'advisory' is the correct word here, since ld.so loads
> DT_DEPAUDIT exactly like DT_AUDIT. Maybe extend that DT_DEPAUDIT is ignore
> if not present.
I knew that. I'll try to word it more clearly.
> > +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.
>
> This could be more concrete: earlier auditors get la_objsearch and
> la_objopen for later auditors and their dependencies (with the lmid of
> the new auditor's namespace), but no la_activity, la_symbind, or
> la_objclose for them.
Given the code that decides this, I'm inclined to think that the
intra-auditor auditing is very buggy and I hesitate to be more
concrete for something that might (should?) change. BTW, la_activity
and la_symbind *do* get called, just not all the times you'd expect.
> > +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.
>
> It is initialized when the link map is created (_dl_new_object), and
> the difference is visible. For dlmopen (LM_ID_NEWLM, ...), la_activity
> (LA_ACT_ADD) is called with the new object's word before its
> la_objopen runs. So callbacks can see the default value (the link map
> pointer) before la_objopen has had a chance to change it.
Sigh, yeah. The API for this part is a mess.
> > +
> > +@deftypefun {unsigned int} la_version (unsigned int version)
>
> I would add '@standards{GNU, link.h}'. Same for all other functions.
I considered that, but libc doesn't provide these functions, and there
are scripts that parse the @standards lines, and I didn't want to
cause problems.
> > +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>}.
>
> This could be a bit more precise: returning 0 unloads the module silently
> (it is only reported with LD_DEBUG=files), while returning a version
> ld.so does not accept prints an error and unloads it. The generic
> check accepts any nonzero value up to LAV_CURRENT, but aarch64 and
> loongarch require exactly LAV_CURRENT (which we might revise since it
> seems a deviation).
They require exact values for a reason. In the future, each arch will
end up with a range of versions that are "compatible" and each auditor
may look for a range it "supports".
> > +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.
>
> Should we list the architecture we its differs?
I see no reason to. There's no reason why each arch couldn't have its
own unique value. If we wanted a consistent API value, we should have
bumped them all at once, but we didn't, and if we did, we'd need more
complicated compatibility logic anyway.
> > +@item LA_SER_SECURE
> > +Unused in Linux.
> > +
>
> I think it is unused on Hurd as well, same for LA_SYMB_STRUCTCALL.
I suppose I could just say "Unused."
> > +@deftypefun uintptr_t la_symbind (Elf_Sym *sym, unsigned int ndx,
> > uintptr_t *refdata, uintptr_t *defdata, unsigned int *flags, const
> > char *symname)
>
> Neither la_symbind nor Elf_Sym exists, and the @deftypefun name ends
> up in the info dir entries as a "GNU C library function". Maybe:
>
> @deftypefun uintptr_t la_symbind32 (Elf32_Sym *@var{sym}, ...)
> @deftypefunx uintptr_t la_symbind64 (Elf64_Sym *@var{sym}, ...)
>
> which also removes the need for the @findex lines.
I was trying to keep it from getting messy, but sure.
> > +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.
>
> It would help to say what the value means: the number of bytes of the
> caller's stack (the stack-passed arguments) that the trampoline copies
> so it can call the function and regain control for la_pltexit. Any
> value >= 0 enables la_pltexit (0 is fine when no arguments are passed
> on the stack), and a value that is too small makes the callee read
> garbage stack arguments. The default, -1, means no la_pltexit.
I couldn't figure out what the value meant, though. Thanks for the
info.
On 25/09/26 17:47, DJ Delorie wrote:
> [initial reply only, for now...]
>
> On Fri, Sep 25, 2026 at 3:50 PM Adhemerval Zanella Netto
> <adhemerval.zanella@linaro.org> wrote:
>> The patch seems to have been line-wrapped by the mailer (the menu context
>> line and the la_objsearch, la_objopen, and la_symbind @deftypefun lines),
>> so it does not apply with git am and/or git-pw.
>
> Sorry. I'm on a new system and haven't figured out how to send email
> via anything other than pasting into a gmail window yet.
>
>> It might be worth adding that this means each auditor also gets its
>> own copy of libc (separate malloc heap, stdio, etc.).
>
> I have an alternate intro from Ben that uses "its own libstdc++" for
> the example.
>
>> We had some issue regarding TLS, so we need to check what is the
>> current support for this.
>
> I submitted a patch for that,it's still pending.
> https://inbox.sourceware.org/libc-alpha/xno6fh4ed4.fsf@greed.delorie.com/
Right, I will try to take care of it (not sure this week due Cauldron).
>
>>> +@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.
>>
>> I am not sure if 'advisory' is the correct word here, since ld.so loads
>> DT_DEPAUDIT exactly like DT_AUDIT. Maybe extend that DT_DEPAUDIT is ignore
>> if not present.
>
> I knew that. I'll try to word it more clearly.
>
>>> +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.
>>
>> This could be more concrete: earlier auditors get la_objsearch and
>> la_objopen for later auditors and their dependencies (with the lmid of
>> the new auditor's namespace), but no la_activity, la_symbind, or
>> la_objclose for them.
>
> Given the code that decides this, I'm inclined to think that the
> intra-auditor auditing is very buggy and I hesitate to be more
> concrete for something that might (should?) change. BTW, la_activity
> and la_symbind *do* get called, just not all the times you'd expect.
Far enough, I take is not an usual setup and it does has some corner cases
issues (I will send a couple of fixes I found).
>
>>> +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.
>>
>> It is initialized when the link map is created (_dl_new_object), and
>> the difference is visible. For dlmopen (LM_ID_NEWLM, ...), la_activity
>> (LA_ACT_ADD) is called with the new object's word before its
>> la_objopen runs. So callbacks can see the default value (the link map
>> pointer) before la_objopen has had a chance to change it.
>
> Sigh, yeah. The API for this part is a mess.
>
>>> +
>>> +@deftypefun {unsigned int} la_version (unsigned int version)
>>
>> I would add '@standards{GNU, link.h}'. Same for all other functions.
>
> I considered that, but libc doesn't provide these functions, and there
> are scripts that parse the @standards lines, and I didn't want to
> cause problems.
I don't expect this would case an issue with script, and link.h does
export such prototypes. But it is a good question whether we should
document interfaces that glibc does not really have proper symbols.
>
>>> +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>}.
>>
>> This could be a bit more precise: returning 0 unloads the module silently
>> (it is only reported with LD_DEBUG=files), while returning a version
>> ld.so does not accept prints an error and unloads it. The generic
>> check accepts any nonzero value up to LAV_CURRENT, but aarch64 and
>> loongarch require exactly LAV_CURRENT (which we might revise since it
>> seems a deviation).
>
> They require exact values for a reason. In the future, each arch will
> end up with a range of versions that are "compatible" and each auditor
> may look for a range it "supports".
>
>>> +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.
>>
>> Should we list the architecture we its differs?
>
> I see no reason to. There's no reason why each arch couldn't have its
> own unique value. If we wanted a consistent API value, we should have
> bumped them all at once, but we didn't, and if we did, we'd need more
> complicated compatibility logic anyway.
Fair enough.
>
>>> +@item LA_SER_SECURE
>>> +Unused in Linux.
>>> +
>>
>> I think it is unused on Hurd as well, same for LA_SYMB_STRUCTCALL.
>
> I suppose I could just say "Unused."
>
>>> +@deftypefun uintptr_t la_symbind (Elf_Sym *sym, unsigned int ndx,
>>> uintptr_t *refdata, uintptr_t *defdata, unsigned int *flags, const
>>> char *symname)
>>
>> Neither la_symbind nor Elf_Sym exists, and the @deftypefun name ends
>> up in the info dir entries as a "GNU C library function". Maybe:
>>
>> @deftypefun uintptr_t la_symbind32 (Elf32_Sym *@var{sym}, ...)
>> @deftypefunx uintptr_t la_symbind64 (Elf64_Sym *@var{sym}, ...)
>>
>> which also removes the need for the @findex lines.
>
> I was trying to keep it from getting messy, but sure.
>
>>> +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.
>>
>> It would help to say what the value means: the number of bytes of the
>> caller's stack (the stack-passed arguments) that the trampoline copies
>> so it can call the function and regain control for la_pltexit. Any
>> value >= 0 enables la_pltexit (0 is fine when no arguments are passed
>> on the stack), and a value that is too small makes the callee read
>> garbage stack arguments. The default, -1, means no la_pltexit.
>
> I couldn't figure out what the value meant, though. Thanks for the
> info.
@@ -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.