From patchwork Thu Sep 24 01:01:35 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: DJ Delorie X-Patchwork-Id: 144533 Return-Path: X-Original-To: patchwork@sourceware.org Delivered-To: patchwork@sourceware.org Received: from vm01.sourceware.org (localhost [IPv6:::1]) by sourceware.org (Postfix) with ESMTP id AF98C4BB3BD3 for ; Thu, 24 Sep 2026 01:02:27 +0000 (GMT) DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org AF98C4BB3BD3 Authentication-Results: sourceware.org; dkim=pass (1024-bit key, unprotected) header.d=redhat.com header.i=@redhat.com header.a=rsa-sha256 header.s=mimecast20190719 header.b=i1OuF6ic X-Original-To: libc-alpha@sourceware.org Delivered-To: libc-alpha@sourceware.org Received: from us-smtp-delivery-124.mimecast.com (us-smtp-delivery-124.mimecast.com [170.10.129.124]) by sourceware.org (Postfix) with ESMTP id D13544BB24EF for ; Thu, 24 Sep 2026 01:01:50 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org D13544BB24EF Authentication-Results: sourceware.org; dmarc=pass (p=quarantine dis=none) header.from=redhat.com Authentication-Results: sourceware.org; spf=pass smtp.mailfrom=redhat.com ARC-Filter: OpenARC Filter v1.0.0 sourceware.org D13544BB24EF Authentication-Results: sourceware.org; arc=none smtp.remote-ip=170.10.129.124 ARC-Seal: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1790211710; cv=none; b=tzYsAqjZkup9lA/xWBSN/qx90kj6pRtlSbe8l8xHCyfcYkHmf0aUMsBWiNrpMJRCTzHFvBR6020RGjncpoNmdS0mHUbsaX1IljqWqKmU57C2kstprr7Ks6bdCeqAfKSOUDlqpfLB/X4owbsyDU9Yp1/PV+5kYGycIsZAxHBGJ7g= ARC-Message-Signature: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1790211710; c=relaxed/simple; bh=pXztFaLBABrfjUKTI5rFTqeZylnZtGcgHns2hN75Xj0=; h=DKIM-Signature:MIME-Version:From:Date:Message-ID:Subject:To; b=nhkjtL3rbBraOqTYnAWtMRRYhnXfwi+xc5Ih3tgJlN+dSvBisEmeU+EZxQEokEMbxClN67AoK92gsrT8atniiwDOyT3fEdtsO+x8EpwxaxjP/UxCWYnXFrI/5ECx3CVMiZ5Tap1UFVCpbnHWs84ADR8WIREX6QNczuxCd8eAtbk= ARC-Authentication-Results: i=1; sourceware.org; dkim=pass (1024-bit key, unprotected) header.d=redhat.com header.i=@redhat.com header.a=rsa-sha256 header.s=mimecast20190719 header.b=i1OuF6ic DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org D13544BB24EF DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com; s=mimecast20190719; t=1790211710; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:mime-version:mime-version:content-type:content-type; bh=p4SHbDGcIS2vIdPPflKKn6D2IMjyqftBBmjEFhudE4Y=; b=i1OuF6icOXzr4nsyHEXIymkuUy0EH2Pf/hRPJByJzyWDSg7niLlaAstjrgCwRKkwAjANTw C/y7tqom+UnRZdlZKRIi2C+1nwD1HlaOlP0nqmvUwyz4QIzcGqj4KTIHzW/kubtyFC6TI0 QW1g5JNukcXMcXCHIp9YRUOx/p0ZIlQ= Received: from mail-qk1-f200.google.com (mail-qk1-f200.google.com [209.85.222.200]) by relay.mimecast.com with ESMTP with STARTTLS (version=TLSv1.3, cipher=TLS_AES_256_GCM_SHA384) id us-mta-563-cPNqNVRHMkqmv-TACK3FKg-1; Wed, 23 Sep 2026 21:01:49 -0400 X-MC-Unique: cPNqNVRHMkqmv-TACK3FKg-1 X-Mimecast-MFC-AGG-ID: cPNqNVRHMkqmv-TACK3FKg_1790211709 Received: by mail-qk1-f200.google.com with SMTP id af79cd13be357-93a082c014aso88464685a.0 for ; Wed, 23 Sep 2026 18:01:49 -0700 (PDT) X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20260707; t=1790211709; x=1790816509; h=content-type:to:subject:message-id:date:from:mime-version:x-gm-gg :x-gm-message-state:from:to:cc:subject:date:message-id:reply-to :content-type; bh=p4SHbDGcIS2vIdPPflKKn6D2IMjyqftBBmjEFhudE4Y=; b=K76T70FD3dwDhkIa+jUWWL1vQqzfuHaf4IhegzBeJ1bROPOZ9NEgr/VhMkRvBHkXi6 QFjAWZsxnK8VHxBrs9iVKbD8Ft/ZRsfuF+PdQIjAgYgPVVVdjnQJ3RaEOY7jY+UWaUf2 LTWfW52kbotTaC6vARJ3FcFNslbuahWfO+5oAFDLWEPcuwjfXjHssIDXKmdV2dhp+/FG Ygbx+FMlz3ypdx3g7WihJ5y5751hEIH/3lBfNRC+j7taFqG+MvgOJC17sEyUNWjXohsd Q+h8NnijmwRu5gJ2a43IfKJGEdO3oX6FqkFhQ1V7z4WZbXPA4/XoLCsuJf1gIqUjx1Aq VrGA== X-Gm-Message-State: AFuF++nPEzpiJ6ftGvFfHZRyueh+g7jiOfSdXEUzkJE6qxYLIhyQaNeV YalZskBq9dQIFg8wP/5mHWw2M8ofc0IJA4d7v++D02O+Q08/+gWdYiDg4x747xfJDHWC7gtQ0il G7EVogyos7P43zWhc720+YQo6lb6205oWsr8jzJIpTXK0y3Rf6nZ5T4KhuxLrE7exqNlkAtGQSE 32FGhy2/nI47k5c9IealUdIIYvmgSb7eL6yyPKvwsgv0dOrMU= X-Gm-Gg: AYBFou1wzoclG1aLU01lVfAZyCiolz9roKb7fobMfHjiMi4Ag4AQnXToHoeW/MMN9Si VLVROlXZ0TTVr5K/YxWA30v5xRv1LHFbxxrWmVuSRD7QSnhohWXa0/K1cFQrAE7uMb5Mxr/T0z8 N8YlcjUP/D/xBxAVWoiKte146+UA0wdRVu9e9cr5rF5m2IFXyDIYswkIbN X-Received: by 2002:a05:620a:4398:b0:93a:d67:9ab0 with SMTP id af79cd13be357-93c36632368mr55060585a.4.1790211708163; Wed, 23 Sep 2026 18:01:48 -0700 (PDT) X-Received: by 2002:a05:620a:4398:b0:93a:d67:9ab0 with SMTP id af79cd13be357-93c36632368mr55056285a.4.1790211707235; Wed, 23 Sep 2026 18:01:47 -0700 (PDT) MIME-Version: 1.0 From: DJ Delorie Date: Wed, 23 Sep 2026 21:01:35 -0400 X-Gm-Features: AclHuK-ID5Z-V0Au79ExeIir4tDmg5Ft4BynBz3gkn6YAoEpMQtWTHE23X8PETY Message-ID: Subject: [PATCH v2 1/1] manual: document audit interface To: libc-alpha X-Mimecast-Spam-Score: 0 X-Mimecast-MFC-PROC-ID: FYpU1rDq6AZBc-f4YjelQb7nR79SM_3--1mGn_Q2WnQ_1790211709 X-Mimecast-Originator: redhat.com X-Spam-Status: No, score=-8.3 required=5.0 tests=BAYES_00, DKIMWL_WL_HIGH, DKIM_SIGNED, DKIM_VALID, DKIM_VALID_AU, DKIM_VALID_EF, GIT_PATCH_0, KAM_ADVERT4, KAM_STOCKGEN, RCVD_IN_DNSWL_NONE, RCVD_IN_MSPIKE_H2, SPF_HELO_PASS, SPF_NONE, TXREP shortcircuit=no autolearn=ham autolearn_force=no version=3.4.6 X-Spam-Checker-Version: SpamAssassin 3.4.6 (2021-04-09) on sourceware.org X-BeenThere: libc-alpha@sourceware.org X-Mailman-Version: 2.1.30 Precedence: list List-Id: Libc-alpha mailing list List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libc-alpha-bounces~patchwork=sourceware.org@sourceware.org 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{} 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{}. + +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{}, 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 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.