diff --git a/meson.build b/meson.build index 8fbf9bbff7..e9c64b098e 100644 --- a/meson.build +++ b/meson.build @@ -571,6 +571,16 @@ have = cc.compiles( name : '__attribute__((__no_reorder__))') conf.set10('HAVE_ATTRIBUTE_NO_REORDER', have) +# The "R" (SHF_GNU_RETAIN) section flag is only understood by binutils >= 2.36. +# TODO: drop when support for CentOS 9 is dropped. +if cc.compiles( + '__asm__(".pushsection .note.test, \\"aR\\", %note\\n.popsection\\n");', + name : 'assembler supports the "R" (SHF_GNU_RETAIN) section flag') + conf.set_quoted('_SD_ELF_NOTE_DLOPEN_SECTION_FLAGS', 'aGR') +else + conf.set_quoted('_SD_ELF_NOTE_DLOPEN_SECTION_FLAGS', 'aG') +endif + ##################################################################### # compilation result tests diff --git a/src/systemd/sd-dlopen.h b/src/systemd/sd-dlopen.h index a58b90e0ec..f9fe9584fb 100644 --- a/src/systemd/sd-dlopen.h +++ b/src/systemd/sd-dlopen.h @@ -27,19 +27,6 @@ extern "C" { #endif -#ifndef _SD_PASTE -# define _SD_PASTE_INNER(a, b) a##b -# define _SD_PASTE(a, b) _SD_PASTE_INNER(a, b) -#endif - -#ifndef _SD_UNIQ -# ifdef __COUNTER__ -# define _SD_UNIQ _SD_PASTE(_sd_uniq_, __COUNTER__) -# else -# define _SD_UNIQ _SD_PASTE(_sd_uniq_, __LINE__) -# endif -#endif - /* ELF note macros implementing the FDO .note.dlopen standard. * * These macros embed metadata in an ELF binary's .note.dlopen section, @@ -63,33 +50,51 @@ extern "C" { #define SD_ELF_NOTE_DLOPEN_PRIORITY_RECOMMENDED "recommended" #define SD_ELF_NOTE_DLOPEN_PRIORITY_SUGGESTED "suggested" -#define _SD_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(SD_ELF_NOTE_DLOPEN_VENDOR)]; \ - _Alignas(sizeof(uint32_t)) char dlopen_json[sizeof(json)]; \ - } variable_name = { \ - .nhdr = { \ - .n_namesz = sizeof(SD_ELF_NOTE_DLOPEN_VENDOR), \ - .n_descsz = sizeof(json), \ - .n_type = SD_ELF_NOTE_DLOPEN_TYPE, \ - }, \ - .name = SD_ELF_NOTE_DLOPEN_VENDOR, \ - .dlopen_json = json, \ - } +/* 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 -#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 "\"]" +/* 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" \ + ".set \"sd_dlopen:" json "\", 1\n" \ + ".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") + +#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_ELF_NOTE_DLOPEN(feature, description, priority, ...) \ - _SD_ELF_NOTE_DLOPEN("[{\"feature\":\"" feature "\",\"description\":\"" description "\",\"priority\":\"" priority "\",\"soname\":" _SD_SONAME_ARRAY(__VA_ARGS__) "}]", _SD_PASTE(_sd_elf_note_dlopen_, _SD_UNIQ)) + _SD_ELF_NOTE_DLOPEN("[{\\\"feature\\\":\\\"" feature "\\\",\\\"description\\\":\\\"" description "\\\",\\\"priority\\\":\\\"" priority "\\\",\\\"soname\\\":" _SD_SONAME_ARRAY(__VA_ARGS__) "}]") #ifdef __cplusplus } diff --git a/test/units/TEST-65-ANALYZE.sh b/test/units/TEST-65-ANALYZE.sh index f98ececc53..a470883aff 100755 --- a/test/units/TEST-65-ANALYZE.sh +++ b/test/units/TEST-65-ANALYZE.sh @@ -1015,10 +1015,24 @@ if systemd-analyze --version | grep -F "+ELFUTILS" >/dev/null; then # For some unknown reason the .note.dlopen sections are removed when building with sanitizers, so only # run this test if we're not running under sanitizers. - if [[ ! -v ASAN_OPTIONS ]]; then + # Also the linker removes them on CentOS 9 as binutils < 2.36 does not support the SHF_GNU_RETAIN flag + if [[ ! -v ASAN_OPTIONS ]] && ( . /etc/os-release; [[ ! "$ID $ID_LIKE" =~ centos|rhel ]] || systemd-analyze compare-versions "$VERSION_ID" ge 10 ); then shared="$(ldd /lib/systemd/systemd | grep shared | cut -d' ' -f3)" systemd-analyze dlopen-metadata "$shared" systemd-analyze dlopen-metadata --json=short "$shared" + + dlopen_json="$(systemd-analyze dlopen-metadata --json=short "$shared")" + assert_eq "$(echo "$dlopen_json" | jq 'type')" '"array"' + assert_ge "$(echo "$dlopen_json" | jq 'length')" 1 + # Every entry must have the four fields with the expected types and a valid priority. + assert_eq "$(echo "$dlopen_json" | jq 'all( + (.feature | type == "string") and + (.description | type == "string") and + (.priority | IN("required", "recommended", "suggested")) and + (.soname | type == "array" and length > 0 and all(type == "string")) + )')" 'true' + # libmount is unconditionally pulled into libsystemd-shared, so its note must be present and correct. + assert_eq "$(echo "$dlopen_json" | jq 'any(.feature == "mount" and (.soname | index("libmount.so.1") != null))')" 'true' fi fi