From: Luca Boccassi <[email protected]> Import MIT-0 sd-dlopen.h header in a new module to add support for the ELF dlopen metadata protocol to programmatically document dlopen deps:
https://uapi-group.org/specifications/specs/elf_dlopen_metadata/ --- lib/sd-dlopen.h | 203 ++++++++++++++++++++++++++++++++++++++++++++++ m4/libdl.m4 | 19 +++-- m4/sd-dlopen.m4 | 31 +++++++ modules/sd-dlopen | 27 ++++++ 4 files changed, 274 insertions(+), 6 deletions(-) create mode 100644 lib/sd-dlopen.h create mode 100644 m4/sd-dlopen.m4 create mode 100644 modules/sd-dlopen diff --git a/lib/sd-dlopen.h b/lib/sd-dlopen.h new file mode 100644 index 0000000000..3f3b275e10 --- /dev/null +++ b/lib/sd-dlopen.h @@ -0,0 +1,203 @@ +/* SPDX-License-Identifier: MIT-0 */ +#ifndef foosddlopenhfoo +#define foosddlopenhfoo + +/*** + Copyright © 2026 The systemd Project + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +***/ + +/* Note: do not include other internal headers from here, to keep it MIT-0 and self-contained */ + +#ifdef __cplusplus +extern "C" { +#endif + +/* ELF note macros implementing the FDO .note.dlopen standard. + * + * These macros embed metadata in an ELF binary's .note.dlopen section, + * declaring optional shared library dependencies that are loaded via + * dlopen() at runtime. Package managers and build systems can read + * these notes to discover runtime dependencies not visible in ELF + * DT_NEEDED entries. + * + * Usage: + * + * SD_ELF_NOTE_DLOPEN("myfeature", "Feature description", + * SD_ELF_NOTE_DLOPEN_PRIORITY_RECOMMENDED, + * "libfoo.so.1"); + * + * See SD_ELF_NOTE_DLOPEN(3) for details. + */ + +#define SD_ELF_NOTE_DLOPEN_VENDOR "FDO" +#define SD_ELF_NOTE_DLOPEN_TYPE 0x407c0c0a +#define SD_ELF_NOTE_DLOPEN_PRIORITY_REQUIRED "required" +#define SD_ELF_NOTE_DLOPEN_PRIORITY_RECOMMENDED "recommended" +#define SD_ELF_NOTE_DLOPEN_PRIORITY_SUGGESTED "suggested" + +#if defined __ELF__ && (defined __GNUC__ || defined __clang__) \ + && defined HAVE_ASM_IFNDEF_ENDIF_BALIGN && HAVE_ASM_IFNDEF_ENDIF_BALIGN + +/* The "R" (SHF_GNU_RETAIN) flag is only understood by binutils >= 2.36, so it can be disabled by defining + * _SD_ELF_NOTE_DLOPEN_SECTION_FLAGS as 'aG' manually, but note that if --gc-sections is used when linking, + * the note will be dropped. */ +#ifndef _SD_ELF_NOTE_DLOPEN_SECTION_FLAGS +# define _SD_ELF_NOTE_DLOPEN_SECTION_FLAGS "aGR" +#endif + +/* This guard tag ensures the assembler folds identical notes when compiling. Unfortunately hppa redefines + * '.equ' so it cannot be used. '.equiv' works on all arches, but it breaks with binutils 2.35 which is used + * on CentOS 9, so we need an ifdef here until the binutils baseline is updated, as above. */ +#if defined(__hppa__) || defined(__hppa64__) +# define _SD_ELF_NOTE_DLOPEN_GUARD ".set" +#else +# define _SD_ELF_NOTE_DLOPEN_GUARD ".equ" +#endif + +/* The json argument is a C string containing already backslash-escaped double quotes (i.e. the bytes + * [{\"feature\"...), so that once it is pasted into the assembler's .asciz directive the assembler decodes + * them back to plain quotes. The note is emitted into a COMDAT group keyed on the payload itself, which + * makes byte-identical notes fold to a single copy: the assembler folds duplicates within a translation + * unit (via the .ifndef guard) and the linker folds them across translation units (via the COMDAT group). + * This way a binary that triggers the same optional dependency from multiple call sites ends up with just + * one note for it. The n_type value below must match SD_ELF_NOTE_DLOPEN_TYPE (it is spelled out as a + * literal here as it cannot be expanded inside the assembler string). */ +#define _SD_ELF_NOTE_DLOPEN(json) \ + __asm__ ( \ + ".ifndef \"sd_dlopen:" json "\"\n" \ + _SD_ELF_NOTE_DLOPEN_GUARD " \"sd_dlopen:" json "\", 1\n" /* guard tag, to fold identical notes */ \ + ".pushsection .note.dlopen, \"" _SD_ELF_NOTE_DLOPEN_SECTION_FLAGS "\", %note, \"sd_dlopen:" json "\", comdat\n" \ + ".balign 4\n" /* notes require 4-byte alignment for the header and fields */ \ + ".long 884f - 883f\n" /* n_namesz: byte length of the vendor name (label 883->884) */ \ + ".long 882f - 881f\n" /* n_descsz: byte length of the JSON payload (label 881->882) */ \ + ".long 0x407c0c0a\n" /* n_type: must match SD_ELF_NOTE_DLOPEN_TYPE */ \ + "883:\n" /* start of vendor name */ \ + ".asciz \"" SD_ELF_NOTE_DLOPEN_VENDOR "\"\n" \ + "884:\n" /* end of vendor name */ \ + ".balign 4\n" \ + "881:\n" /* start of JSON payload */ \ + ".asciz \"" json "\"\n" \ + "882:\n" /* end of JSON payload */ \ + ".balign 4\n" \ + ".popsection\n" \ + ".endif\n") + +/* + * SD_ELF_NOTE_DLOPEN_ANCHORED() emits a .note.dlopen ELF note that is "anchored" to a dummy symbol via the + * SHF_LINK_ORDER ('o') section flag, in addition to SHF_GROUP ('G') for folding identical notes together. + * + * Unlike the plain SD_ELF_NOTE_DLOPEN() macro, this variant ties the note's lifetime to a dummy symbol named + * with the specified tag: if the linker's --gc-sections removes the function that calls the macro (e.g. + * because the function is never referenced), the associated .note.dlopen entry is garbage-collected along + * with it. + * + * USAGE NOTE: + * - The 'tag' argument must be a unique symbol name to characterize the dlopen note (e.g. generated from + * feature and its priority). + * + * Example: + * void dlopen_libfoo(int log_level) { + * SD_ELF_NOTE_DLOPEN_ANCHORED(foo_suggested, "foo", "description of foo", "suggested", "libfoo.so.0"); + * ... + * } + * + * TOOLCHAIN REQUIREMENTS: + * - GNU as (binutils) >= 2.35: this is when support for specifying the SHF_LINK_ORDER ('o') associated + * symbol as a direct argument to .section/.pushsection was added. + * - LLVM (clang -integrated-as) >= 18: matching support for the 'o' flag argument combined with 'G' + * (SHF_GROUP) landed around LLVM 18. + * - Do NOT add 'R' (SHF_GNU_RETAIN) to these flags: combined with SHF_LINK_ORDER, it would prevent the + * linker from ever garbage-collecting the note, defeating the whole purpose of anchoring it to a symbol in + * the first place. + */ +#define _SD_ELF_NOTE_DLOPEN_ANCHORED(tag, json) \ + _Pragma("GCC diagnostic push") \ + _Pragma("GCC diagnostic ignored \"-Wnested-externs\"") \ + _Pragma("GCC diagnostic ignored \"-Wredundant-decls\"") \ + __attribute__((visibility("hidden"))) \ + extern volatile const char __sd_dlopen_anchor_##tag __asm__("__sd_dlopen_anchor_" #tag); \ + _Pragma("GCC diagnostic pop") \ + __asm__ ( \ + ".ifndef \"sd_dlopen:emitted:" #tag "\"\n" \ + _SD_ELF_NOTE_DLOPEN_GUARD " \"sd_dlopen:emitted:" #tag "\", 1\n" \ + ".pushsection .data.sd_dlopen_dummy." #tag ", \"awG\", %progbits, sd_dlopen_group_" #tag ", comdat\n" \ + ".weak __sd_dlopen_anchor_" #tag "\n" \ + ".hidden __sd_dlopen_anchor_" #tag "\n" \ + ".type __sd_dlopen_anchor_" #tag ", %object\n" \ + ".balign 8\n" \ + "__sd_dlopen_anchor_" #tag ":\n" \ + ".byte 0\n" \ + ".popsection\n" \ + ".pushsection .note.dlopen, \"aGo\", %note, __sd_dlopen_anchor_" #tag ", sd_dlopen_group_" #tag ", comdat\n" \ + ".balign 4\n" \ + ".long 884f - 883f\n" \ + ".long 882f - 881f\n" \ + ".long 0x407c0c0a\n" \ + "883:\n" \ + ".asciz \"" SD_ELF_NOTE_DLOPEN_VENDOR "\"\n" \ + "884:\n" \ + ".balign 4\n" \ + "881:\n" \ + ".asciz \"" json "\"\n" \ + "882:\n" \ + ".balign 4\n" \ + ".popsection\n" \ + ".endif\n" \ + ); \ + __asm__ volatile ("" : : "r" (&__sd_dlopen_anchor_##tag)) + +#define _SD_SONAME_ARRAY1(a) "[\\\"" a "\\\"]" +#define _SD_SONAME_ARRAY2(a, b) "[\\\"" a "\\\",\\\"" b "\\\"]" +#define _SD_SONAME_ARRAY3(a, b, c) "[\\\"" a "\\\",\\\"" b "\\\",\\\"" c "\\\"]" +#define _SD_SONAME_ARRAY4(a, b, c, d) "[\\\"" a "\\\",\\\"" b "\\\",\\\"" c "\\\",\\\"" d "\\\"]" +#define _SD_SONAME_ARRAY5(a, b, c, d, e) "[\\\"" a "\\\",\\\"" b "\\\",\\\"" c "\\\",\\\"" d "\\\",\\\"" e "\\\"]" +#define _SD_SONAME_ARRAY_GET(_1,_2,_3,_4,_5,NAME,...) NAME +#define _SD_SONAME_ARRAY(...) _SD_SONAME_ARRAY_GET(__VA_ARGS__, _SD_SONAME_ARRAY5, _SD_SONAME_ARRAY4, _SD_SONAME_ARRAY3, _SD_SONAME_ARRAY2, _SD_SONAME_ARRAY1)(__VA_ARGS__) + +#define _SD_DLOPEN_JSON(feature, description, priority, ...) \ + "[{" \ + "\\\"feature\\\":\\\"" feature "\\\"," \ + "\\\"description\\\":\\\"" description "\\\"," \ + "\\\"priority\\\":\\\"" priority "\\\"," \ + "\\\"soname\\\":" _SD_SONAME_ARRAY(__VA_ARGS__) \ + "}]" + +#define SD_ELF_NOTE_DLOPEN(feature, description, priority, ...) \ + _SD_ELF_NOTE_DLOPEN(_SD_DLOPEN_JSON(feature, description, priority, __VA_ARGS__)) + +/* The anchored note requires LLVM >= 18 (see above). Fall back to the non-anchored note on older clang. */ +#if defined(__clang__) && !defined(__apple_build_version__) && __clang_major__ < 18 +# define SD_ELF_NOTE_DLOPEN_ANCHORED(tag, feature, description, priority, ...) \ + SD_ELF_NOTE_DLOPEN(feature, description, priority, __VA_ARGS__) +#else +# define SD_ELF_NOTE_DLOPEN_ANCHORED(tag, feature, description, priority, ...) \ + _SD_ELF_NOTE_DLOPEN_ANCHORED(tag, _SD_DLOPEN_JSON(feature, description, priority, __VA_ARGS__)) +#endif + +#else + +#define SD_ELF_NOTE_DLOPEN(feature, description, priority, ...) +#define SD_ELF_NOTE_DLOPEN_ANCHORED(tag, feature, description, priority, ...) + +#endif + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/m4/libdl.m4 b/m4/libdl.m4 index 79d6906363..b24fad832b 100644 --- a/m4/libdl.m4 +++ b/m4/libdl.m4 @@ -1,5 +1,5 @@ # libdl.m4 -# serial 3 +# serial 4 dnl Copyright (C) 2024-2026 Free Software Foundation, Inc. dnl This file is free software; the Free Software Foundation dnl gives unlimited permission to copy and/or distribute it, @@ -10,6 +10,7 @@ dnl From Bruno Haible. dnl Set LIBDL to '-ldl' if it is needed to use the functions declared dnl in <dlfcn.h> (dlopen, dlsym, etc.), or to empty otherwise. +dnl Set HAVE_DLOPEN to 1 if dlopen and dlsym are available, or to 0 otherwise. AC_DEFUN([gl_LIBDL], [ dnl dlopen, dlsym are @@ -22,7 +23,8 @@ AC_DEFUN([gl_LIBDL], [AC_LANG_PROGRAM( [[#include <dlfcn.h> ]], - [[return ! dlsym (RTLD_DEFAULT, "main");]])], + [[void *handle = dlopen (NULL, RTLD_LAZY); + return ! (handle && dlsym (handle, "main"));]])], [gl_cv_lib_dl=none], [gl_cv_lib_dl=maybe]) if test $gl_cv_lib_dl = maybe; then @@ -32,15 +34,20 @@ AC_DEFUN([gl_LIBDL], [AC_LANG_PROGRAM( [[#include <dlfcn.h> ]], - [[return ! dlsym (RTLD_DEFAULT, "main");]])], + [[void *handle = dlopen (NULL, RTLD_LAZY); + return ! (handle && dlsym (handle, "main"));]])], [gl_cv_lib_dl='-ldl'], - [gl_cv_lib_dl=none]) + [gl_cv_lib_dl=no]) LIBS="$saved_LIBS" fi ]) case "$gl_cv_lib_dl" in - none) LIBDL='' ;; - *) LIBDL="$gl_cv_lib_dl" ;; + none) LIBDL=''; HAVE_DLOPEN=1 ;; + no) LIBDL=''; HAVE_DLOPEN=0 ;; + *) LIBDL="$gl_cv_lib_dl"; HAVE_DLOPEN=1 ;; esac AC_SUBST([LIBDL]) + AC_SUBST([HAVE_DLOPEN]) + AC_DEFINE_UNQUOTED([HAVE_DLOPEN], [$HAVE_DLOPEN], + [Define to 1 if dlopen and dlsym are available.]) ]) diff --git a/m4/sd-dlopen.m4 b/m4/sd-dlopen.m4 new file mode 100644 index 0000000000..716c67dd86 --- /dev/null +++ b/m4/sd-dlopen.m4 @@ -0,0 +1,31 @@ +# sd-dlopen.m4 +# serial 1 +dnl Copyright (C) 2026 Free Software Foundation, Inc. +dnl This file is free software; the Free Software Foundation +dnl gives unlimited permission to copy and/or distribute it, +dnl with or without modifications, as long as this notice is preserved. +dnl This file is offered as-is, without any warranty. + +AC_DEFUN([gl_SD_DLOPEN], +[ + AC_REQUIRE([gl_LIBDL]) + + AC_CACHE_CHECK([whether the assembler supports .ifndef, .endif, and .balign], + [gl_cv_asm_ifndef_endif_balign], + [AC_COMPILE_IFELSE( + [AC_LANG_SOURCE([[ +#if !defined __ELF__ || !(defined __GNUC__ || defined __clang__) +# error "not an ELF target with GNU-style inline assembly" +#endif +__asm__ (".ifndef \"gl_sd_dlopen_test\"\n" + ".balign 4\n" + ".endif\n"); +]])], + [gl_cv_asm_ifndef_endif_balign=yes], + [gl_cv_asm_ifndef_endif_balign=no])]) + if test $gl_cv_asm_ifndef_endif_balign = yes; then + AC_DEFINE([HAVE_ASM_IFNDEF_ENDIF_BALIGN], [1], + [Define to 1 if the assembler supports .ifndef, .endif, and .balign.]) + fi +]) + diff --git a/modules/sd-dlopen b/modules/sd-dlopen new file mode 100644 index 0000000000..179a1f1ef5 --- /dev/null +++ b/modules/sd-dlopen @@ -0,0 +1,27 @@ +Description: +Emit ELF notes for package dependencies loaded with dlopen. + +Files: +lib/sd-dlopen.h +m4/sd-dlopen.m4 +m4/libdl.m4 + +Depends-on: +stdint-h + +configure.ac: +gl_SD_DLOPEN + +Makefile.am: + +Include: +"sd-dlopen.h" + +Link: +$(LIBDL) + +License: +LGPLv2+ + +Maintainer: +all -- 2.47.3
