Merge pull request #32234 from poettering/dlopen-name-elf-note

Expose dlopen() dependencies in an ELF section, and add spec for it
This commit is contained in:
Luca Boccassi 2024-05-08 13:56:34 +02:00 committed by GitHub
commit 344ededcea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 253 additions and 0 deletions

View file

@ -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 |

View file

@ -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',

View file

@ -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,

View file

@ -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<X+1> 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))

View file

@ -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,

View file

@ -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),

View file

@ -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);

View file

@ -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),

View file

@ -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),

View file

@ -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,

View file

@ -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 */

View file

@ -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",

View file

@ -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),

View file

@ -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",

View file

@ -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),

View file

@ -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),

View file

@ -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

View file

@ -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,

View file

@ -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,

View file

@ -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),