summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-06-12 04:25:26 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-06-12 04:25:26 +0000
commit5ceacb82619304a784634ee5bb0ad0ea4e54ab3c (patch)
tree3b5e6fd489662166a27092ed563714d2a678576a
parentInitial commit. (diff)
downloadpackage-notes-5ceacb82619304a784634ee5bb0ad0ea4e54ab3c.tar.xz
package-notes-5ceacb82619304a784634ee5bb0ad0ea4e54ab3c.zip
Adding upstream version 0.10~exp.upstream/0.10_exp
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
-rw-r--r--.github/workflows/tests.yml25
-rw-r--r--.pytest_cache/CACHEDIR.TAG4
-rw-r--r--.pytest_cache/README.md8
-rw-r--r--.pytest_cache/v/cache/lastfailed1
-rw-r--r--.pytest_cache/v/cache/nodeids3
-rw-r--r--.pytest_cache/v/cache/stepwise1
-rw-r--r--.vscode/settings.json6
-rw-r--r--Makefile10
-rw-r--r--README.md17
-rwxr-xr-xdlopen-notes.py165
-rw-r--r--hello.spec74
-rw-r--r--rpm/macros.package-notes-srpm30
-rw-r--r--rpm/redhat-package-notes.in2
-rw-r--r--test/.pytest_cache/CACHEDIR.TAG4
-rw-r--r--test/.pytest_cache/README.md8
-rw-r--r--test/.pytest_cache/v/cache/lastfailed3
-rw-r--r--test/.pytest_cache/v/cache/nodeids3
-rw-r--r--test/.pytest_cache/v/cache/stepwise1
-rw-r--r--test/Makefile9
l---------test/_notes.py1
-rw-r--r--test/notes.c50
-rw-r--r--test/test.py15
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