diff --git a/docs/ELF_DLOPEN_METADATA.md b/docs/ELF_DLOPEN_METADATA.md new file mode 100644 index 0000000000..5c3bf1eae2 --- /dev/null +++ b/docs/ELF_DLOPEN_METADATA.md @@ -0,0 +1,89 @@ +--- +title: Dlopen Metadata for ELF Files +category: Interfaces +layout: default +SPDX-License-Identifier: LGPL-2.1-or-later +--- + +# `dlopen()` Metadata for ELF Files + +*Intended audience: hackers working on packaging ELF files that use dlopen to load libraries.* + +## Motivation + +Using `dlopen()` to load optional dependencies brings several advantages: programs can gracefully downgrade +a feature when a library is not available, and the shared library is only loaded into the process (and its +ELF constructors are run) only when the requested feature is actually used. But it also has some drawbacks, +and the main one is that it is harder to track a program's dependencies, since unlike build-time dynamic +linking there will not be a mention in the ELF metadata. This specification aims to solve this problem by +providing a standardized specification for a custom ELF note that can be used to list `dlopen()` +dependencies. + +## Implementation + +This document will attempt to define a common metadata format specification, so that multiple implementers +might use it when coding upstream software, and packagers might use it when building packages and setting +dependencies. + +The metadata will be embedded in a series of new, 4-byte-aligned, allocated, 0-padded, read-only ELF header +sections, in a JSON array containing name-value objects, either one ELF note per dependency or as a single +note listing multiple dependencies in the top-level array. Implementers working on parsing ELF files should +not assume a specific list of names, but parse anything that is included in the section, and should look for +the note using the `note type`. Implementers working on build tools should strive to use the same names, for +consistency. The most common will be listed here. + +* Section header + +``` +SECTION: `.note.dlopen` +note type: `0x407c0c0a` +Owner: `FDO` (FreeDesktop.org) +Value: an array of JSON objects encoded as a zero-terminated UTF-8 string +``` + +* JSON payload + +```json +[ + { + "soname": ["libfoo.so.1"], + "feature": "foo", + "description": "Enables the foo feature", + "priority": "recommended" + } +] +``` + +The format is a single JSON array containing objects, encoded as a zero-terminated `UTF-8` string. Each key +in each object shall be unique as per recommendations of [RFC8259](https://datatracker.ietf.org/doc/html/rfc8259#section-4). +Strings shall not contain any control characters or use `\uXXX` escaping. + +Reference implementations of [packaging tools for `.deb` and `.rpm`](https://github.com/systemd/package-notes) +are available, and provide macros/helpers to parse the note when building packages and adding dependencies. + +## Well-known keys + +The metadata format is intentionally extensible, so that upstreams and later revisions of this spec can add +their own information. The 'soname' array is required, with at least one element, everything else is +optional. If alternative soname versions for the same library are supported at the same time, an array can +be used, listing the most preferred first, and parsers are expected to select only the first one that is +available on the system, as it is a mechanism to specify alternatives. If the `priority` field is used, it +must follow the specification and use one of the values specified in the table. If it is not specified, a +parser should assume 'recommended' if a priority is needed. If the `feature` field is used, it will identify +an individual feature, and multiple entries using the same `feature` denote functionality that requires all +of the libraries they specify in order to be enabled. + +| Key name | Key type | Mandatory | Key description | Example value | +|-------------|----------------------------|-----------|--------------------------------------------------------------------------|----------------------------------| +| soname | array of strings | yes | The library names loaded by `dlopen()` | [ "libfoo.so.1", "libfoo.so.0" ] | +| feature | string | no | A keyword identifying the feature that the library contributes to enable | "foo" | +| description | string | no | A human-readable text string describing the feature | "Enables the foo feature" | +| priority | string | no | The priority of the feature, one of: required, recommended, suggested | "recommended" | + +### Priority definition + +| Priority | Semantics | +|-------------|--------------------------------------------------------------------------------------------------------------------------------------| +| required | Core functionality needs the dependency, the binary will not work if it cannot be found | +| recommended | Important functionality needs the dependency, the binary will work but in most cases the dependency should be provided | +| suggested | Secondary functionality needs the dependency, the binary will work and the dependency is only needed for full-featured installations | diff --git a/meson.build b/meson.build index 63c358194d..6ea9fce85a 100644 --- a/meson.build +++ b/meson.build @@ -1418,6 +1418,15 @@ elif compression == 'lz4' and not liblz4.found() elif compression == 'xz' and not libxz.found() error('default-compression=xz requires xz') endif +# In the dlopen ELF note we save the default compression library with a +# higher priority, so that packages can give it priority over the +# secondary libraries. +conf.set_quoted('COMPRESSION_PRIORITY_ZSTD', + compression == 'zstd' ? 'recommended' : 'suggested') +conf.set_quoted('COMPRESSION_PRIORITY_LZ4', + compression == 'lz4' ? 'recommended' : 'suggested') +conf.set_quoted('COMPRESSION_PRIORITY_XZ', + compression == 'xz' ? 'recommended' : 'suggested') conf.set('DEFAULT_COMPRESSION', 'COMPRESSION_@0@'.format(compression.to_upper())) libarchive = dependency('libarchive', diff --git a/src/basic/compress.c b/src/basic/compress.c index 5a4293fa81..33b27d3b99 100644 --- a/src/basic/compress.c +++ b/src/basic/compress.c @@ -129,6 +129,11 @@ bool compression_supported(Compression c) { #if HAVE_XZ int dlopen_lzma(void) { + ELF_NOTE_DLOPEN("lzma", + "Support lzma compression in journal and coredump files", + COMPRESSION_PRIORITY_XZ, + "liblzma.so.5"); + return dlopen_many_sym_or_warn( &lzma_dl, "liblzma.so.5", LOG_DEBUG, @@ -186,6 +191,11 @@ int compress_blob_xz(const void *src, uint64_t src_size, #if HAVE_LZ4 int dlopen_lz4(void) { + ELF_NOTE_DLOPEN("lz4", + "Support lz4 compression in journal and coredump files", + COMPRESSION_PRIORITY_LZ4, + "liblz4.so.1"); + return dlopen_many_sym_or_warn( &lz4_dl, "liblz4.so.1", LOG_DEBUG, @@ -242,6 +252,11 @@ int compress_blob_lz4(const void *src, uint64_t src_size, #if HAVE_ZSTD int dlopen_zstd(void) { + ELF_NOTE_DLOPEN("zstd", + "Support zstd compression in journal and coredump files", + COMPRESSION_PRIORITY_ZSTD, + "libzstd.so.1"); + return dlopen_many_sym_or_warn( &zstd_dl, "libzstd.so.1", LOG_DEBUG, diff --git a/src/basic/dlfcn-util.h b/src/basic/dlfcn-util.h index 050f1e2da7..b395d4ca04 100644 --- a/src/basic/dlfcn-util.h +++ b/src/basic/dlfcn-util.h @@ -39,3 +39,44 @@ int dlopen_many_sym_or_warn_sentinel(void **dlp, const char *filename, int log_l /* libbpf is a bit confused about type-safety and API compatibility. Provide a macro that can tape over that mess. Sad. */ #define DLSYM_ARG_FORCE(arg) \ &sym_##arg, STRINGIFY(arg) + +#define ELF_NOTE_DLOPEN_VENDOR "FDO" +#define ELF_NOTE_DLOPEN_TYPE UINT32_C(0x407c0c0a) +#define ELF_NOTE_DLOPEN_PRIORITY_REQUIRED "required" +#define ELF_NOTE_DLOPEN_PRIORITY_RECOMMENDED "recommended" +#define ELF_NOTE_DLOPEN_PRIORITY_SUGGESTED "suggested" + +/* Add an ".note.dlopen" ELF note to our binary that declares our weak dlopen() dependency. This + * information can be read from an ELF file via "readelf -p .note.dlopen" or an equivalent command. */ +#define _ELF_NOTE_DLOPEN(json, variable_name) \ + __attribute__((used, section(".note.dlopen"))) _Alignas(sizeof(uint32_t)) static const struct { \ + struct { \ + uint32_t n_namesz, n_descsz, n_type; \ + } nhdr; \ + char name[sizeof(ELF_NOTE_DLOPEN_VENDOR)]; \ + _Alignas(sizeof(uint32_t)) char dlopen_json[sizeof(json)]; \ + } variable_name = { \ + .nhdr = { \ + .n_namesz = sizeof(ELF_NOTE_DLOPEN_VENDOR), \ + .n_descsz = sizeof(json), \ + .n_type = ELF_NOTE_DLOPEN_TYPE, \ + }, \ + .name = ELF_NOTE_DLOPEN_VENDOR, \ + .dlopen_json = json, \ + } + +#define _SONAME_ARRAY1(a) "[\""a"\"]" +#define _SONAME_ARRAY2(a, b) "[\""a"\",\""b"\"]" +#define _SONAME_ARRAY3(a, b, c) "[\""a"\",\""b"\",\""c"\"]" +#define _SONAME_ARRAY4(a, b, c, d) "[\""a"\",\""b"\",\""c"\"",\""d"\"]" +#define _SONAME_ARRAY5(a, b, c, d, e) "[\""a"\",\""b"\",\""c"\"",\""d"\",\""e"\"]" +#define _SONAME_ARRAY_GET(_1,_2,_3,_4,_5,NAME,...) NAME +#define _SONAME_ARRAY(...) _SONAME_ARRAY_GET(__VA_ARGS__, _SONAME_ARRAY5, _SONAME_ARRAY4, _SONAME_ARRAY3, _SONAME_ARRAY2, _SONAME_ARRAY1)(__VA_ARGS__) + +/* The 'priority' must be one of 'required', 'recommended' or 'suggested' as per specification, use the + * macro defined above to specify it. + * Multiple sonames can be passed and they will be automatically contructed into a json array (but note that + * due to preprocessor language limitations if more than the limit defined above is used, a new + * _SONAME_ARRAY will need to be added). */ +#define ELF_NOTE_DLOPEN(feature, description, priority, ...) \ + _ELF_NOTE_DLOPEN("[{\"feature\":\"" feature "\",\"description\":\"" description "\",\"priority\":\"" priority "\",\"soname\":" _SONAME_ARRAY(__VA_ARGS__) "}]", UNIQ_T(s, UNIQ)) diff --git a/src/basic/gcrypt-util.c b/src/basic/gcrypt-util.c index 081866537c..4d68d2c22b 100644 --- a/src/basic/gcrypt-util.c +++ b/src/basic/gcrypt-util.c @@ -41,6 +41,11 @@ DLSYM_FUNCTION(gcry_randomize); DLSYM_FUNCTION(gcry_strerror); static int dlopen_gcrypt(void) { + ELF_NOTE_DLOPEN("gcrypt", + "Support for journald forward-sealing", + ELF_NOTE_DLOPEN_PRIORITY_SUGGESTED, + "libgcrypt.so.20"); + return dlopen_many_sym_or_warn( &gcrypt_dl, "libgcrypt.so.20", LOG_DEBUG, diff --git a/src/locale/xkbcommon-util.c b/src/locale/xkbcommon-util.c index 468452f8af..d4e24cd0dd 100644 --- a/src/locale/xkbcommon-util.c +++ b/src/locale/xkbcommon-util.c @@ -16,6 +16,10 @@ DLSYM_FUNCTION(xkb_keymap_new_from_names); DLSYM_FUNCTION(xkb_keymap_unref); static int dlopen_xkbcommon(void) { + ELF_NOTE_DLOPEN("xkbcommon", + "Support for keyboard locale descriptions", + ELF_NOTE_DLOPEN_PRIORITY_SUGGESTED, "libxkbcommon.so.0"); + return dlopen_many_sym_or_warn( &xkbcommon_dl, "libxkbcommon.so.0", LOG_DEBUG, DLSYM_ARG(xkb_context_new), diff --git a/src/shared/bpf-dlopen.c b/src/shared/bpf-dlopen.c index de3e40516e..d4ae24053e 100644 --- a/src/shared/bpf-dlopen.c +++ b/src/shared/bpf-dlopen.c @@ -76,6 +76,11 @@ int dlopen_bpf(void) { void *dl; int r; + ELF_NOTE_DLOPEN("bpf", + "Support firewalling and sandboxing with BPF", + ELF_NOTE_DLOPEN_PRIORITY_SUGGESTED, + "libbpf.so.1", "libbpf.so.0"); + DISABLE_WARNING_DEPRECATED_DECLARATIONS; dl = dlopen("libbpf.so.1", RTLD_LAZY); diff --git a/src/shared/cryptsetup-util.c b/src/shared/cryptsetup-util.c index cbbc85a5cc..288e6e8942 100644 --- a/src/shared/cryptsetup-util.c +++ b/src/shared/cryptsetup-util.c @@ -252,6 +252,11 @@ int dlopen_cryptsetup(void) { DISABLE_WARNING_DEPRECATED_DECLARATIONS; + ELF_NOTE_DLOPEN("cryptsetup", + "Support for disk encryption, integrity, and authentication", + ELF_NOTE_DLOPEN_PRIORITY_SUGGESTED, + "libcryptsetup.so.12"); + r = dlopen_many_sym_or_warn( &cryptsetup_dl, "libcryptsetup.so.12", LOG_DEBUG, DLSYM_ARG(crypt_activate_by_passphrase), diff --git a/src/shared/elf-util.c b/src/shared/elf-util.c index 85f3c173db..9d1f494671 100644 --- a/src/shared/elf-util.c +++ b/src/shared/elf-util.c @@ -87,6 +87,11 @@ static DLSYM_FUNCTION(gelf_getnote); int dlopen_dw(void) { int r; + ELF_NOTE_DLOPEN("dw", + "Support for backtrace and ELF package metadata decoding from core files", + ELF_NOTE_DLOPEN_PRIORITY_SUGGESTED, + "libdw.so.1"); + r = dlopen_many_sym_or_warn( &dw_dl, "libdw.so.1", LOG_DEBUG, DLSYM_ARG(dwarf_getscopes), @@ -130,6 +135,11 @@ int dlopen_dw(void) { int dlopen_elf(void) { int r; + ELF_NOTE_DLOPEN("elf", + "Support for backtraces and reading ELF package metadata from core files", + ELF_NOTE_DLOPEN_PRIORITY_SUGGESTED, + "libelf.so.1"); + r = dlopen_many_sym_or_warn( &elf_dl, "libelf.so.1", LOG_DEBUG, DLSYM_ARG(elf_begin), diff --git a/src/shared/firewall-util-iptables.c b/src/shared/firewall-util-iptables.c index 044d2d0744..b0abccc26b 100644 --- a/src/shared/firewall-util-iptables.c +++ b/src/shared/firewall-util-iptables.c @@ -354,6 +354,11 @@ int fw_iptables_add_local_dnat( } static int dlopen_iptc(void) { + ELF_NOTE_DLOPEN("ip4tc", + "Support for firewall rules", + ELF_NOTE_DLOPEN_PRIORITY_SUGGESTED, + "libip4tc.so.2"); + return dlopen_many_sym_or_warn( &iptc_dl, "libip4tc.so.2", LOG_DEBUG, diff --git a/src/shared/idn-util.c b/src/shared/idn-util.c index 6b26f2064a..aa88e11221 100644 --- a/src/shared/idn-util.c +++ b/src/shared/idn-util.c @@ -21,6 +21,11 @@ const char *(*sym_idn2_strerror)(int rc) _const_ = NULL; DLSYM_FUNCTION(idn2_to_unicode_8z8z); int dlopen_idn(void) { + ELF_NOTE_DLOPEN("idn", + "Support for internationalized domain names", + ELF_NOTE_DLOPEN_PRIORITY_SUGGESTED, + "libidn2.so.0"); + return dlopen_many_sym_or_warn( &idn_dl, "libidn2.so.0", LOG_DEBUG, DLSYM_ARG(idn2_lookup_u8), @@ -39,6 +44,11 @@ int dlopen_idn(void) { _cleanup_(dlclosep) void *dl = NULL; int r; + ELF_NOTE_DLOPEN("idn", + "Support for internationalized domain names", + ELF_NOTE_DLOPEN_PRIORITY_SUGGESTED, + "libidn.so.12", "libidn.so.11"); + if (idn_dl) return 0; /* Already loaded */ diff --git a/src/shared/libarchive-util.c b/src/shared/libarchive-util.c index e6d6581597..58f6554da2 100644 --- a/src/shared/libarchive-util.c +++ b/src/shared/libarchive-util.c @@ -30,6 +30,11 @@ DLSYM_FUNCTION(archive_write_set_format_filter_by_ext); DLSYM_FUNCTION(archive_write_set_format_gnutar); int dlopen_libarchive(void) { + ELF_NOTE_DLOPEN("archive", + "Support for decompressing archive files", + ELF_NOTE_DLOPEN_PRIORITY_SUGGESTED, + "libarchive.so.13"); + return dlopen_many_sym_or_warn( &libarchive_dl, "libarchive.so.13", diff --git a/src/shared/libfido2-util.c b/src/shared/libfido2-util.c index 17fb019ffc..37f6898925 100644 --- a/src/shared/libfido2-util.c +++ b/src/shared/libfido2-util.c @@ -72,6 +72,11 @@ static void fido_log_propagate_handler(const char *s) { int dlopen_libfido2(void) { int r; + ELF_NOTE_DLOPEN("fido2", + "Support fido2 for encryption and authentication", + ELF_NOTE_DLOPEN_PRIORITY_SUGGESTED, + "libfido2.so.1"); + r = dlopen_many_sym_or_warn( &libfido2_dl, "libfido2.so.1", LOG_DEBUG, DLSYM_ARG(fido_assert_allow_cred), diff --git a/src/shared/module-util.c b/src/shared/module-util.c index 612ab9c9c5..fa1e0f82bb 100644 --- a/src/shared/module-util.c +++ b/src/shared/module-util.c @@ -25,6 +25,11 @@ DLSYM_FUNCTION(kmod_unref); DLSYM_FUNCTION(kmod_validate_resources); int dlopen_libkmod(void) { + ELF_NOTE_DLOPEN("kmod", + "Support for loading kernel modules", + ELF_NOTE_DLOPEN_PRIORITY_RECOMMENDED, + "libkmod.so.2"); + return dlopen_many_sym_or_warn( &libkmod_dl, "libkmod.so.2", diff --git a/src/shared/password-quality-util-passwdqc.c b/src/shared/password-quality-util-passwdqc.c index c32b493534..764b7724b0 100644 --- a/src/shared/password-quality-util-passwdqc.c +++ b/src/shared/password-quality-util-passwdqc.c @@ -20,6 +20,11 @@ DLSYM_FUNCTION(passwdqc_check); DLSYM_FUNCTION(passwdqc_random); int dlopen_passwdqc(void) { + ELF_NOTE_DLOPEN("passwdqc", + "Support for password quality checks", + ELF_NOTE_DLOPEN_PRIORITY_SUGGESTED, + "libpasswdqc.so.1"); + return dlopen_many_sym_or_warn( &passwdqc_dl, "libpasswdqc.so.1", LOG_DEBUG, DLSYM_ARG(passwdqc_params_reset), diff --git a/src/shared/password-quality-util-pwquality.c b/src/shared/password-quality-util-pwquality.c index e070f9636c..7456469c86 100644 --- a/src/shared/password-quality-util-pwquality.c +++ b/src/shared/password-quality-util-pwquality.c @@ -24,6 +24,11 @@ DLSYM_FUNCTION(pwquality_set_int_value); DLSYM_FUNCTION(pwquality_strerror); int dlopen_pwquality(void) { + ELF_NOTE_DLOPEN("pwquality", + "Support for password quality checks", + ELF_NOTE_DLOPEN_PRIORITY_SUGGESTED, + "libpwquality.so.1"); + return dlopen_many_sym_or_warn( &pwquality_dl, "libpwquality.so.1", LOG_DEBUG, DLSYM_ARG(pwquality_check), diff --git a/src/shared/pcre2-util.c b/src/shared/pcre2-util.c index 4f33efc5fc..7deb64fd49 100644 --- a/src/shared/pcre2-util.c +++ b/src/shared/pcre2-util.c @@ -27,6 +27,11 @@ const struct hash_ops pcre2_code_hash_ops_free = {}; int dlopen_pcre2(void) { #if HAVE_PCRE2 + ELF_NOTE_DLOPEN("pcre2", + "Support for regular expressions", + ELF_NOTE_DLOPEN_PRIORITY_SUGGESTED, + "libpcre2-8.so.0"); + /* So here's something weird: PCRE2 actually renames the symbols exported by the library via C * macros, so that the exported symbols carry a suffix "_8" but when used from C the suffix is * gone. In the argument list below we ignore this mangling. Surprisingly (at least to me), we diff --git a/src/shared/pkcs11-util.c b/src/shared/pkcs11-util.c index 8077ec3019..b5cd9a35bb 100644 --- a/src/shared/pkcs11-util.c +++ b/src/shared/pkcs11-util.c @@ -61,6 +61,11 @@ DLSYM_FUNCTION(p11_kit_uri_new); DLSYM_FUNCTION(p11_kit_uri_parse); int dlopen_p11kit(void) { + ELF_NOTE_DLOPEN("p11-kit", + "Support for PKCS11 hardware tokens", + ELF_NOTE_DLOPEN_PRIORITY_SUGGESTED, + "libp11-kit.so.0"); + return dlopen_many_sym_or_warn( &p11kit_dl, "libp11-kit.so.0", LOG_DEBUG, diff --git a/src/shared/qrcode-util.c b/src/shared/qrcode-util.c index c087136bd2..e62a5a8635 100644 --- a/src/shared/qrcode-util.c +++ b/src/shared/qrcode-util.c @@ -24,6 +24,11 @@ static DLSYM_FUNCTION(QRcode_free); int dlopen_qrencode(void) { int r; + ELF_NOTE_DLOPEN("qrencode", + "Support for generating QR codes", + ELF_NOTE_DLOPEN_PRIORITY_SUGGESTED, + "libqrencode.so.4", "libqrencode.so.3"); + FOREACH_STRING(s, "libqrencode.so.4", "libqrencode.so.3") { r = dlopen_many_sym_or_warn( &qrcode_dl, s, LOG_DEBUG, diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index 3afeb09516..a64c2738bf 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -113,6 +113,11 @@ static DLSYM_FUNCTION(Tss2_RC_Decode); int dlopen_tpm2(void) { int r; + ELF_NOTE_DLOPEN("tpm", + "Support for TPM", + ELF_NOTE_DLOPEN_PRIORITY_SUGGESTED, + "libtss2-esys.so.0"); + r = dlopen_many_sym_or_warn( &libtss2_esys_dl, "libtss2-esys.so.0", LOG_DEBUG, DLSYM_ARG(Esys_Create), @@ -164,12 +169,22 @@ int dlopen_tpm2(void) { if (r < 0) log_debug("libtss2-esys too old, does not include Esys_TR_GetTpmHandle."); + ELF_NOTE_DLOPEN("tpm", + "Support for TPM", + ELF_NOTE_DLOPEN_PRIORITY_SUGGESTED, + "libtss2-rc.so.0"); + r = dlopen_many_sym_or_warn( &libtss2_rc_dl, "libtss2-rc.so.0", LOG_DEBUG, DLSYM_ARG(Tss2_RC_Decode)); if (r < 0) return r; + ELF_NOTE_DLOPEN("tpm", + "Support for TPM", + ELF_NOTE_DLOPEN_PRIORITY_SUGGESTED, + "libtss2-mu.so.0"); + return dlopen_many_sym_or_warn( &libtss2_mu_dl, "libtss2-mu.so.0", LOG_DEBUG, DLSYM_ARG(Tss2_MU_TPM2_CC_Marshal),