diff options
-rw-r--r-- | .github/workflows/tests.yml | 25 | ||||
-rw-r--r-- | .pytest_cache/CACHEDIR.TAG | 4 | ||||
-rw-r--r-- | .pytest_cache/README.md | 8 | ||||
-rw-r--r-- | .pytest_cache/v/cache/lastfailed | 1 | ||||
-rw-r--r-- | .pytest_cache/v/cache/nodeids | 3 | ||||
-rw-r--r-- | .pytest_cache/v/cache/stepwise | 1 | ||||
-rw-r--r-- | .vscode/settings.json | 6 | ||||
-rw-r--r-- | Makefile | 10 | ||||
-rw-r--r-- | README.md | 17 | ||||
-rwxr-xr-x | dlopen-notes.py | 165 | ||||
-rw-r--r-- | hello.spec | 74 | ||||
-rw-r--r-- | rpm/macros.package-notes-srpm | 30 | ||||
-rw-r--r-- | rpm/redhat-package-notes.in | 2 | ||||
-rw-r--r-- | test/.pytest_cache/CACHEDIR.TAG | 4 | ||||
-rw-r--r-- | test/.pytest_cache/README.md | 8 | ||||
-rw-r--r-- | test/.pytest_cache/v/cache/lastfailed | 3 | ||||
-rw-r--r-- | test/.pytest_cache/v/cache/nodeids | 3 | ||||
-rw-r--r-- | test/.pytest_cache/v/cache/stepwise | 1 | ||||
-rw-r--r-- | test/Makefile | 9 | ||||
l--------- | test/_notes.py | 1 | ||||
-rw-r--r-- | test/notes.c | 50 | ||||
-rw-r--r-- | test/test.py | 15 |
22 files changed, 440 insertions, 0 deletions
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..4b39fba --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,25 @@ +--- +# SPDX-License-Identifier: CC0-1.0 +# vi: ts=2 sw=2 et: + +name: Run tests +on: [pull_request] + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-22.04 + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + strategy: + fail-fast: false + steps: + - name: Repository checkout + uses: actions/checkout@v4 + - name: Install dependencies + run: sudo apt -y update && sudo apt -y install python3-pyelftools python3-pytest + - name: Run tests + run: make check diff --git a/.pytest_cache/CACHEDIR.TAG b/.pytest_cache/CACHEDIR.TAG new file mode 100644 index 0000000..fce15ad --- /dev/null +++ b/.pytest_cache/CACHEDIR.TAG @@ -0,0 +1,4 @@ +Signature: 8a477f597d28d172789f06886806bc55 +# This file is a cache directory tag created by pytest. +# For information about cache directory tags, see: +# https://bford.info/cachedir/spec.html diff --git a/.pytest_cache/README.md b/.pytest_cache/README.md new file mode 100644 index 0000000..b89018c --- /dev/null +++ b/.pytest_cache/README.md @@ -0,0 +1,8 @@ +# pytest cache directory # + +This directory contains data from the pytest's cache plugin, +which provides the `--lf` and `--ff` options, as well as the `cache` fixture. + +**Do not** commit this to version control. + +See [the docs](https://docs.pytest.org/en/stable/how-to/cache.html) for more information. diff --git a/.pytest_cache/v/cache/lastfailed b/.pytest_cache/v/cache/lastfailed new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/.pytest_cache/v/cache/lastfailed @@ -0,0 +1 @@ +{}
\ No newline at end of file diff --git a/.pytest_cache/v/cache/nodeids b/.pytest_cache/v/cache/nodeids new file mode 100644 index 0000000..5e6a081 --- /dev/null +++ b/.pytest_cache/v/cache/nodeids @@ -0,0 +1,3 @@ +[ + "test/test.py::test_notes" +]
\ No newline at end of file diff --git a/.pytest_cache/v/cache/stepwise b/.pytest_cache/v/cache/stepwise new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/.pytest_cache/v/cache/stepwise @@ -0,0 +1 @@ +[]
\ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..cd318de --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "files.associations": { + "random": "c", + "functional": "cpp" + } +}
\ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..13de305 --- /dev/null +++ b/Makefile @@ -0,0 +1,10 @@ +all: + +install: + install -m 755 -D dlopen-notes.py $(DESTDIR)/usr/bin/dlopen-notes + +check: + make -C test check + +clean: + make -C test clean diff --git a/README.md b/README.md new file mode 100644 index 0000000..6dccd9d --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +ELF Package Notes Reference Implementation + +## Description + +This repository provides RPM and DEB packaging tools to generate an ELF note +that will be linked into compiled binaries (programs and shared libraries) to +provide metadata about the package for which the binary was compiled. + +See [Package Metadata for Core Files](https://systemd.io/ELF_PACKAGE_METADATA/) +for the overview and details. + +The new `--package-metadata` option provided by bfd, gold, mold and lld is used. + +## Requirements +* binutils (>= 2.39) +* mold (>= 1.3.0) +* lld (>= 15.0.0) diff --git a/dlopen-notes.py b/dlopen-notes.py new file mode 100755 index 0000000..29ea270 --- /dev/null +++ b/dlopen-notes.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: CC0-1.0 + +"""\ +Read .note.dlopen notes from ELF files and report the contents +""" + +import argparse +import enum +import functools +import json +import sys +from elftools.elf.elffile import ELFFile +from elftools.elf.sections import NoteSection + +try: + import rich + print_json = rich.print_json +except ImportError: + print_json = print + +def listify(f): + def wrap(*args, **kwargs): + return list(f(*args, **kwargs)) + return functools.update_wrapper(wrap, f) + +@listify +def read_dlopen_notes(filename): + elffile = ELFFile(open(filename, 'rb')) + + for section in elffile.iter_sections(): + if not isinstance(section, NoteSection) or section.name != '.note.dlopen': + continue + + for note in section.iter_notes(): + if note['n_type'] != 0x407c0c0a or note['n_name'] != 'FDO': + continue + note_desc = note['n_desc'] + + try: + # On older Python versions (e.g.: Ubuntu 22.04) we get a string, on + # newer versions a bytestring + if not isinstance(note_desc, str): + text = note_desc.decode('utf-8').rstrip('\0') + else: + text = note_desc.rstrip('\0') + except UnicodeDecodeError as e: + raise ValueError(f'{filename}: Invalid UTF-8 in .note.dlopen n_desc') from e + + try: + j = json.loads(text) + except json.JSONDecodeError as e: + raise ValueError(f'{filename}: Invalid JSON in .note.dlopen note_desc') from e + + if not isinstance(j, list): + print(f'{filename}: ignoring .note.dlopen n_desc with JSON that is not a list', + file=sys.stderr) + continue + + yield from j + +def dictify(f): + def wrap(*args, **kwargs): + return dict(f(*args, **kwargs)) + return functools.update_wrapper(wrap, f) + +@dictify +def group_by_soname(notes): + for note in notes: + for element in note: + priority = element.get('priority', 'recommended') + for soname in element['soname']: + yield soname, priority + +class Priority(enum.Enum): + suggested = 1 + recommended = 2 + required = 3 + + def __lt__(self, other): + return self.value < other.value + +def group_by_feature(filenames, notes): + features = {} + + # We expect each note to be in the format: + # [ + # { + # "feature": "...", + # "description": "...", + # "priority": "required"|"recommended"|"suggested", + # "soname": ["..."], + # } + # ] + for filename, note_group in zip(filenames, notes): + for note in note_group: + prio = Priority[note.get('priority', 'recommened')] + feature_name = note['feature'] + + try: + feature = features[feature_name] + except KeyError: + # Create new + feature = features[feature_name] = { + 'description': note.get('description', ''), + 'sonames': { soname:prio for soname in note['soname'] }, + } + else: + # Merge + if feature['description'] != note.get('description', ''): + print(f"{filename}: feature {note['feature']!r} found with different description, ignoring", + file=sys.stderr) + + for soname in note['soname']: + highest = max(feature['sonames'].get(soname, Priority.suggested), + prio) + feature['sonames'][soname] = highest + + return features + +def parse_args(): + p = argparse.ArgumentParser(description=__doc__) + p.add_argument('--raw', + action='store_true', + help='show the original JSON extracted from input files') + p.add_argument('--sonames', + action='store_true', + help='list all sonames and their priorities, one soname per line') + p.add_argument('--features', + nargs='?', + const=[], + type=lambda s: s.split(','), + action='extend', + metavar='FEATURE1,FEATURE2', + help='describe features, can be specified multiple times') + p.add_argument('filenames', nargs='+', metavar='filename') + return p.parse_args() + +if __name__ == '__main__': + args = parse_args() + + notes = [read_dlopen_notes(filename) for filename in args.filenames] + + if args.raw: + for filename, note in zip(args.filenames, notes): + print(f'# {filename}') + print_json(json.dumps(note, indent=2)) + + if args.features is not None: + features = group_by_feature(args.filenames, notes) + + toprint = {name:feature for name,feature in features.items() + if name in args.features or not args.features} + if len(toprint) < len(args.features): + sys.exit('Some features were not found') + + print('# grouped by feature') + print_json(json.dumps(toprint, + indent=2, + default=lambda prio: prio.name)) + + if args.sonames: + sonames = group_by_soname(notes) + for soname in sorted(sonames.keys()): + print(f"{soname} {sonames[soname]}") diff --git a/hello.spec b/hello.spec new file mode 100644 index 0000000..4cf3ebd --- /dev/null +++ b/hello.spec @@ -0,0 +1,74 @@ +# SPDX-License-Identifier: CC0-1.0 + +%bcond_without notes + +Name: hello +Version: 0 +Release: 1%{?dist}%{!?with_notes:.nonotes} +Summary: Aloha! + +License: CC0 + +BuildRequires: binutils >= 2.39 +BuildRequires: gcc +BuildRequires: rpmdevtools + +Source0: rpm/redhat-package-notes.in + +%description +Test with: +objdump -s -j .note.package %{_bindir}/hello +objdump -s -j .note.package %{_libdir}/libhello.so + +%prep +%setup -cT +set -eo pipefail + +cat <<EOF >libhello.c +const char* greeting(void) { + return "Hello"; +} +EOF +cat <<EOF >hello.c +#include <stdio.h> +extern char* greeting(void); +int main() { + puts(greeting()); + return 0; +} +EOF + +%build +set -eo pipefail + +%if %{with notes} +sed "s|@OSCPE@|$(cat /usr/lib/system-release-cpe)|" %{SOURCE0} >redhat-package-notes +%endif + +LDFLAGS="%{build_ldflags} %{?with_notes:-specs=$PWD/redhat-package-notes}" +CFLAGS="%{build_cflags}" + +gcc -Wall -fPIC -o libhello.so -shared libhello.c $CFLAGS $LDFLAGS +gcc -Wall -o hello hello.c libhello.so $CFLAGS $LDFLAGS + +%install +set -eo pipefail + +install -Dt %{buildroot}%{_libdir}/ libhello.so +install -Dt %{buildroot}%{_bindir}/ hello + +%check +set -eo pipefail + +%if %{with notes} +objdump -s -j .note.package ./hello +objdump -s -j .note.package ./libhello.so +%endif + +%files +%{_bindir}/hello +%{_libdir}/libhello.so + +%changelog +* Wed Feb 3 2021 Zbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl> - 0-1 +- Test diff --git a/rpm/macros.package-notes-srpm b/rpm/macros.package-notes-srpm new file mode 100644 index 0000000..a15d98c --- /dev/null +++ b/rpm/macros.package-notes-srpm @@ -0,0 +1,30 @@ +# SPDX-License-Identifier: CC0-1.0 +# +# This file is part of the package-notes package. +# +# Add an ELF note with information about the package the code was compiled for. +# See https://fedoraproject.org/wiki/Changes/Package_information_on_ELF_objects +# for details. +# +# To opt out of the use of this feature completely, include this in the spec +# file: +# +# %undefine _package_note_flags +# +# Which linker will be used? This should be either "bfd", "gold", "mold", or "lld". +# +# (The default linker for clang on armv7hl is lld.) +%_package_note_linker %["%_target_cpu" == "armv7hl" && "%{toolchain}" == "clang" ? "lld" : "bfd"] + +# These are defined for backwards compatibility. Do not use. +%_package_note_file 1 +%_generate_package_note_file %{nil} + +# Overall status: 1 if looks like we can insert the note, 0 otherwise +# Unfortunately "clang" does not support specs files so the note insertion is disabled when using it. +%_package_note_status %[0%{?_package_note_file:1} && 0%{?name:1} && "%_target_cpu" != "noarch" && "%{toolchain}" != "clang" ? 1 : 0] + +# The linker flags to be passed to the compiler to insert the notes section will +# be created by the spec file, to avoid issues with quoting and escaping across +# different build systems and shells. +%_package_note_flags %[%_package_note_status ? "-specs=/usr/lib/rpm/redhat/redhat-package-notes" : ""] diff --git a/rpm/redhat-package-notes.in b/rpm/redhat-package-notes.in new file mode 100644 index 0000000..3a19b1b --- /dev/null +++ b/rpm/redhat-package-notes.in @@ -0,0 +1,2 @@ +*link: ++ --package-metadata={\"type\":\"rpm\",\"name\":\"%:getenv(RPM_PACKAGE_NAME \",\"version\":\"%:getenv(RPM_PACKAGE_VERSION -%:getenv(RPM_PACKAGE_RELEASE \",\"architecture\":\"%:getenv(RPM_ARCH \",\"osCpe\":\"@OSCPE@\"})))) diff --git a/test/.pytest_cache/CACHEDIR.TAG b/test/.pytest_cache/CACHEDIR.TAG new file mode 100644 index 0000000..fce15ad --- /dev/null +++ b/test/.pytest_cache/CACHEDIR.TAG @@ -0,0 +1,4 @@ +Signature: 8a477f597d28d172789f06886806bc55 +# This file is a cache directory tag created by pytest. +# For information about cache directory tags, see: +# https://bford.info/cachedir/spec.html diff --git a/test/.pytest_cache/README.md b/test/.pytest_cache/README.md new file mode 100644 index 0000000..b89018c --- /dev/null +++ b/test/.pytest_cache/README.md @@ -0,0 +1,8 @@ +# pytest cache directory # + +This directory contains data from the pytest's cache plugin, +which provides the `--lf` and `--ff` options, as well as the `cache` fixture. + +**Do not** commit this to version control. + +See [the docs](https://docs.pytest.org/en/stable/how-to/cache.html) for more information. diff --git a/test/.pytest_cache/v/cache/lastfailed b/test/.pytest_cache/v/cache/lastfailed new file mode 100644 index 0000000..f974275 --- /dev/null +++ b/test/.pytest_cache/v/cache/lastfailed @@ -0,0 +1,3 @@ +{ + "test.py::test_notes": true +}
\ No newline at end of file diff --git a/test/.pytest_cache/v/cache/nodeids b/test/.pytest_cache/v/cache/nodeids new file mode 100644 index 0000000..baac9f6 --- /dev/null +++ b/test/.pytest_cache/v/cache/nodeids @@ -0,0 +1,3 @@ +[ + "test.py::test_notes" +]
\ No newline at end of file diff --git a/test/.pytest_cache/v/cache/stepwise b/test/.pytest_cache/v/cache/stepwise new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/test/.pytest_cache/v/cache/stepwise @@ -0,0 +1 @@ +[]
\ No newline at end of file diff --git a/test/Makefile b/test/Makefile new file mode 100644 index 0000000..b91a4ac --- /dev/null +++ b/test/Makefile @@ -0,0 +1,9 @@ +notes: notes.c + $(CC) -o $@ $+ $(CFLAGS) $(LDFLAGS) $(LDLIBS) + +check: notes + python3 -m pytest test.py + +clean: + rm -f notes + rm -rf __pycache__/ diff --git a/test/_notes.py b/test/_notes.py new file mode 120000 index 0000000..88fcc3d --- /dev/null +++ b/test/_notes.py @@ -0,0 +1 @@ +../dlopen-notes.py
\ No newline at end of file diff --git a/test/notes.c b/test/notes.c new file mode 100644 index 0000000..5fa9dc1 --- /dev/null +++ b/test/notes.c @@ -0,0 +1,50 @@ +/* SPDX-License-Identifier: CC0-1.0 */ + +#include <stdint.h> + +#define XCONCATENATE(x, y) x ## y +#define CONCATENATE(x, y) XCONCATENATE(x, y) +#define UNIQ_T(x, uniq) CONCATENATE(__unique_prefix_, CONCATENATE(x, uniq)) +#define UNIQ __COUNTER__ + +#define ELF_NOTE_DLOPEN_VENDOR "FDO" +#define ELF_NOTE_DLOPEN_TYPE 0x407c0c0a + +#define _ELF_NOTE_DLOPEN(module, 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_module[sizeof(module)]; \ + } variable_name = { \ + .nhdr = { \ + .n_namesz = sizeof(ELF_NOTE_DLOPEN_VENDOR), \ + .n_descsz = sizeof(module), \ + .n_type = ELF_NOTE_DLOPEN_TYPE, \ + }, \ + .name = ELF_NOTE_DLOPEN_VENDOR, \ + .dlopen_module = module, \ + } + +#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__) + +#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)) + +#define ELF_NOTE_DLOPEN_DUAL(feature0, priority0, module0, feature1, priority1, module1) \ + _ELF_NOTE_DLOPEN("[{\"feature\":\"" feature0 "\",\"priority\":\"" priority0 "\",\"soname\":[\"" module0 "\"]}, {\"feature\":\"" feature1 "\",\"priority\":\"" priority1 "\",\"soname\":[\"" module1 "\"]}]", UNIQ_T(s, UNIQ)) + +int main(int argc, char **argv) { + ELF_NOTE_DLOPEN("fido2", "Support fido2 for encryption and authentication.", "required", "libfido2.so.1"); + ELF_NOTE_DLOPEN("pcre2", "Support pcre2 for regex", "suggested", "libpcre2-8.so.0","libpcre2-8.so.1"); + ELF_NOTE_DLOPEN("lz4", "Support lz4 decompression in journal and coredump files", "recommended", "liblz4.so.1"); + ELF_NOTE_DLOPEN_DUAL("tpm", "recommended", "libtss2-mu.so.0", "tpm", "recommended", "libtss2-esys.so.0"); + return 0; +} diff --git a/test/test.py b/test/test.py new file mode 100644 index 0000000..462f476 --- /dev/null +++ b/test/test.py @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: CC0-1.0 + +from _notes import read_dlopen_notes, group_by_soname + +def test_notes(): + expected = { + 'libfido2.so.1': 'required', + 'liblz4.so.1': 'recommended', + 'libpcre2-8.so.0': 'suggested', + 'libpcre2-8.so.1': 'suggested', + 'libtss2-esys.so.0': 'recommended', + 'libtss2-mu.so.0': 'recommended', + } + notes = [read_dlopen_notes('notes')] + assert group_by_soname(notes) == expected |