From a94fbe0d2796aecb1321905697205502dc08fcad Mon Sep 17 00:00:00 2001 From: zhouganqing Date: Fri, 11 Nov 2022 17:27:06 +0800 Subject: [PATCH] Import Upstream version 0.8 --- .../super-linter-links/dh/dh_package_notes.pl | 1 + .github/workflows/linter.yml | 44 +++ .github/workflows/tests.yml | 29 ++ README.md | 26 ++ dh/dh_package_notes | 85 ++++++ dh/package-notes.mk | 4 + dh/package_notes.pm | 10 + generate-package-notes.py | 222 ++++++++++++++ generate-package-notes.sh | 287 ++++++++++++++++++ hello.spec | 85 ++++++ man/generate-package-notes.1 | 136 +++++++++ rpm/macros.package-notes-srpm | 57 ++++ tests/_generate_package_notes.py | 1 + tests/resources/__init__.py | 1 + tests/resources/fedora-cpe-os-release.ld | 25 ++ tests/resources/fedora-cpe-system-release.ld | 25 ++ tests/resources/fedora-long-name.ld | 88 ++++++ tests/resources/fedora-package.ld | 33 ++ .../resources/root-no-cpe/usr/lib/os-release | 1 + tests/resources/root/usr/lib/os-release | 1 + .../resources/root/usr/lib/system-release-cpe | 1 + tests/resources/very-short-rw.ld | 29 ++ tests/resources/very-short.ld | 29 ++ tests/test_basics.py | 54 ++++ tests/test_basics.sh | 13 + 25 files changed, 1287 insertions(+) create mode 120000 .github/super-linter-links/dh/dh_package_notes.pl create mode 100644 .github/workflows/linter.yml create mode 100644 .github/workflows/tests.yml create mode 100644 README.md create mode 100755 dh/dh_package_notes create mode 100644 dh/package-notes.mk create mode 100755 dh/package_notes.pm create mode 100755 generate-package-notes.py create mode 100755 generate-package-notes.sh create mode 100644 hello.spec create mode 100644 man/generate-package-notes.1 create mode 100644 rpm/macros.package-notes-srpm create mode 120000 tests/_generate_package_notes.py create mode 100644 tests/resources/__init__.py create mode 100644 tests/resources/fedora-cpe-os-release.ld create mode 100644 tests/resources/fedora-cpe-system-release.ld create mode 100644 tests/resources/fedora-long-name.ld create mode 100644 tests/resources/fedora-package.ld create mode 100644 tests/resources/root-no-cpe/usr/lib/os-release create mode 100644 tests/resources/root/usr/lib/os-release create mode 100644 tests/resources/root/usr/lib/system-release-cpe create mode 100644 tests/resources/very-short-rw.ld create mode 100644 tests/resources/very-short.ld create mode 100644 tests/test_basics.py create mode 100755 tests/test_basics.sh diff --git a/.github/super-linter-links/dh/dh_package_notes.pl b/.github/super-linter-links/dh/dh_package_notes.pl new file mode 120000 index 0000000..1db1a04 --- /dev/null +++ b/.github/super-linter-links/dh/dh_package_notes.pl @@ -0,0 +1 @@ +../../../dh/dh_package_notes \ No newline at end of file diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml new file mode 100644 index 0000000..133ee57 --- /dev/null +++ b/.github/workflows/linter.yml @@ -0,0 +1,44 @@ +--- +# SPDX-License-Identifier: CC0-1.0 +# vi: ts=2 sw=2 et: + +name: Lint Code Base +on: [pull_request] + +jobs: + build: + runs-on: ubuntu-latest + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + strategy: + fail-fast: false + steps: + - name: Repository checkout + uses: actions/checkout@v2 + with: + # We need a full repo clone to get a correct list of changed files + fetch-depth: 0 + # FIXME: super-linter doesn't recognize perl/python scripts without + # an extension; let's workaround it by creating a dummy symlink + # directory with necessary extensions until it's resolved. + # The `find` below just checks if all symlinks in the dummy + # directory are valid. + - name: Check test-related symlinks + run: find .github/super-linter-links/ -type l -exec ls -L {} + + - name: Lint Code Base + uses: github/super-linter/slim@latest + env: + DEFAULT_BRANCH: main + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + MULTI_STATUS: true + # FIXME: until the above FIXME is resolved, we need to always check + # the whole codebase, otherwise only changed files would be + # considered, which would ignore the symlinked files. Since + # we have only couple of files to check, it's not an issue + # (yet). + VALIDATE_ALL_CODEBASE: true + VALIDATE_BASH: true + VALIDATE_PERL: true + VALIDATE_PYTHON_ISORT: true + VALIDATE_PYTHON_PYLINT: true diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..7551a39 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,29 @@ +--- +# 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-20.04 + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + strategy: + fail-fast: false + steps: + - name: Repository checkout + uses: actions/checkout@v2 + - name: Install dependencies + run: sudo apt -y update && sudo apt -y install binutils python3-pytest python3-simplejson devscripts + - name: Run python tests + run: python3 -m pytest -v + - name: Run shell tests + run: ./tests/test_basics.sh + - name: Check shell script for bashism + run: checkbashisms ./tests/test_basics.sh diff --git a/README.md b/README.md new file mode 100644 index 0000000..de53094 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +ELF Package Notes Reference Implementation + +## Description + +This repository provides a script to generate an ELF note that can 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/COREDUMP_PACKAGE_METADATA/) +for the overview and details. + +We provide implementations in Python and POSIX shell, with compatible CLI +interfaces. + +## Requirements + +### generate-package-notes.py + +* python3 (>= 3.5) +* python3-simplejson +* binutils (>= 2.38) [--readonly true] + +### generate-package-notes.sh + +* POSIX shell +* binutils (>= 2.38) [--readonly true] diff --git a/dh/dh_package_notes b/dh/dh_package_notes new file mode 100755 index 0000000..e0323a7 --- /dev/null +++ b/dh/dh_package_notes @@ -0,0 +1,85 @@ +#!/usr/bin/perl -w +# SPDX-License-Identifier: CC0-1.0 + +=head1 NAME + +dh_package_notes - Add package metadata to ELF header + +=cut + +use strict; +use warnings; +use Debian::Debhelper::Dh_Lib + qw(%dh doit hostarch init isnative sourcepackage); +use Dpkg::Vendor qw(get_current_vendor); +our $VERSION = '0.8'; + +=head1 SYNOPSIS + +B + +=cut + +=head1 DESCRIPTION + +B is a debhelper program that creates a linker script to +include package metadata in ELF binaries built by packages. + +The package metadata specification for ELF binaries can be found at +L. + +B creates a linker script in a fixed location at +B with the package type set to B, the package +name and version set to the source package name and version respectively, and +the B and B fields set to the values of the B and +B fields found in B. Simply add +B to the Build-Depends or add +B<--with package_notes> to the B call to make this happen. + +The package using B also needs to manually set in debian/rules +B as it is +not possible to do so from a debhelper addon. + +=cut + +init(); + +isnative( $dh{MAINPACKAGE} ); # Necessary to have $dh{VERSION} +my $source_package = sourcepackage(); +my $package_arch = hostarch(); +my $vendor = get_current_vendor(); +my $url = $ENV{'DEBUGINFOD_URLS'}; +if (!defined $url) { + $url = "https://debuginfod.debian.net"; +} +my %options = ( 'stdout' => "debian/.debhelper/notes.ld" ); +my @cmd = ( + "/usr/bin/generate-package-notes", + "--type", "deb", + "--os", lc ${vendor}, + "--name", ${source_package}, + "--architecture", ${package_arch}, + "--version", $dh{VERSION}, + "--debugInfoUrl", ${url} +); + +if ( not mkdir("debian/.debhelper") ) { + error("mkdir debian/.debhelper failed: $!"); +} + +doit( \%options, @cmd ); + +# FIXME: neither of these work, so appending from each debian/rules file is +# currently the only way to make this work, which is less than ideal +#$ENV{'DEB_LDFLAGS_MAINT_APPEND'} .= ' -Wl,-dT,debian/.debhelper/notes.ld'; +#$ENV{'LDFLAGS'} .= ' -Wl,-dT,debian/.debhelper/notes.ld'; + +=head1 SEE ALSO + +L + +=head1 AUTHOR + +Luca Boccassi + +=cut diff --git a/dh/package-notes.mk b/dh/package-notes.mk new file mode 100644 index 0000000..1dfa595 --- /dev/null +++ b/dh/package-notes.mk @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: CC0-1.0 +# Include from debian/rules to use with dh_package_notes. +# See dh_package_notes(1) for details +export DEB_LDFLAGS_MAINT_APPEND+= -Wl,-dT,$(CURDIR)/debian/.debhelper/notes.ld diff --git a/dh/package_notes.pm b/dh/package_notes.pm new file mode 100755 index 0000000..7a963aa --- /dev/null +++ b/dh/package_notes.pm @@ -0,0 +1,10 @@ +#!/usr/bin/perl +# SPDX-License-Identifier: CC0-1.0 + +use warnings; +use strict; +use Debian::Debhelper::Dh_Lib; + +insert_before('dh_auto_configure', 'dh_package_notes'); + +1 diff --git a/generate-package-notes.py b/generate-package-notes.py new file mode 100755 index 0000000..1f70b3e --- /dev/null +++ b/generate-package-notes.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: CC0-1.0 + +""" +Generates a linker script to insert a .note.package section with a +JSON payload. The contents are derived from the specified options and the +os-release file. Use the output with -Wl,-dT,/path/to/output in $LDFLAGS. + +$ ./generate-package-notes.py --package-type rpm --package-name systemd --package-version 248~rc2-1.fc34 --package-architecture x86_64 --cpe 'cpe:/o:fedoraproject:fedora:33' +SECTIONS +{ + .note.package (READONLY) : ALIGN(4) { + BYTE(0x04) BYTE(0x00) BYTE(0x00) BYTE(0x00) /* Length of Owner including NUL */ + BYTE(0x7c) BYTE(0x00) BYTE(0x00) BYTE(0x00) /* Length of Value including NUL */ + BYTE(0x7e) BYTE(0x1a) BYTE(0xfe) BYTE(0xca) /* Note ID */ + BYTE(0x46) BYTE(0x44) BYTE(0x4f) BYTE(0x00) /* Owner: 'FDO\x00' */ + BYTE(0x7b) BYTE(0x22) BYTE(0x74) BYTE(0x79) /* Value: '{"type":"rpm","name":"systemd","version":"248~rc2-1.fc34","architecture":"x86_64","osCpe":"cpe:/o:fedoraproject:fedora:33"}\x00' */ + BYTE(0x70) BYTE(0x65) BYTE(0x22) BYTE(0x3a) + BYTE(0x22) BYTE(0x72) BYTE(0x70) BYTE(0x6d) + BYTE(0x22) BYTE(0x2c) BYTE(0x22) BYTE(0x6e) + BYTE(0x61) BYTE(0x6d) BYTE(0x65) BYTE(0x22) + BYTE(0x3a) BYTE(0x22) BYTE(0x73) BYTE(0x79) + BYTE(0x73) BYTE(0x74) BYTE(0x65) BYTE(0x6d) + BYTE(0x64) BYTE(0x22) BYTE(0x2c) BYTE(0x22) + BYTE(0x76) BYTE(0x65) BYTE(0x72) BYTE(0x73) + BYTE(0x69) BYTE(0x6f) BYTE(0x6e) BYTE(0x22) + BYTE(0x3a) BYTE(0x22) BYTE(0x32) BYTE(0x34) + BYTE(0x38) BYTE(0x7e) BYTE(0x72) BYTE(0x63) + BYTE(0x32) BYTE(0x2d) BYTE(0x31) BYTE(0x2e) + BYTE(0x66) BYTE(0x63) BYTE(0x33) BYTE(0x34) + BYTE(0x22) BYTE(0x2c) BYTE(0x22) BYTE(0x61) + BYTE(0x72) BYTE(0x63) BYTE(0x68) BYTE(0x69) + BYTE(0x74) BYTE(0x65) BYTE(0x63) BYTE(0x74) + BYTE(0x75) BYTE(0x72) BYTE(0x65) BYTE(0x22) + BYTE(0x3a) BYTE(0x22) BYTE(0x78) BYTE(0x38) + BYTE(0x36) BYTE(0x5f) BYTE(0x36) BYTE(0x34) + BYTE(0x22) BYTE(0x2c) BYTE(0x22) BYTE(0x6f) + BYTE(0x73) BYTE(0x43) BYTE(0x70) BYTE(0x65) + BYTE(0x22) BYTE(0x3a) BYTE(0x22) BYTE(0x63) + BYTE(0x70) BYTE(0x65) BYTE(0x3a) BYTE(0x2f) + BYTE(0x6f) BYTE(0x3a) BYTE(0x66) BYTE(0x65) + BYTE(0x64) BYTE(0x6f) BYTE(0x72) BYTE(0x61) + BYTE(0x70) BYTE(0x72) BYTE(0x6f) BYTE(0x6a) + BYTE(0x65) BYTE(0x63) BYTE(0x74) BYTE(0x3a) + BYTE(0x66) BYTE(0x65) BYTE(0x64) BYTE(0x6f) + BYTE(0x72) BYTE(0x61) BYTE(0x3a) BYTE(0x33) + BYTE(0x33) BYTE(0x22) BYTE(0x7d) BYTE(0x00) + } +} +INSERT AFTER .note.gnu.build-id; +/* HINT: add -Wl,-dT,/path/to/this/file to $LDFLAGS */ + +See https://systemd.io/COREDUMP_PACKAGE_METADATA/ for details. +""" + +__version__ = '0.8' + +import argparse +import itertools +import os +import re +from pathlib import Path + +import simplejson as json + +DOC_PARAGRAPHS = ['\n'.join(group) + for (key, group) in itertools.groupby(__doc__.splitlines(), bool) + if key] + +def read_os_release(field, root=Path('/')): + try: + f = open(root / 'etc/os-release') + except FileNotFoundError: + f = open(root / 'usr/lib/os-release') + + prefix = '{}='.format(field) + for line in f: + if line.startswith(prefix): + break + else: + return None + + value = line.rstrip() + value = value[value.startswith(prefix) and len(prefix):] + if value[0] in '"\'' and value[0] == value[-1]: + value = value[1:-1] + + return value + +def str_to_bool(v): + if isinstance(v, bool): + return v + if v.lower() in {'yes', 'true', '1'}: + return True + if v.lower() in {'no', 'false', '0'}: + return False + raise argparse.ArgumentTypeError('"yes"/"true"/"1"/"no"/"false"/"0" expected') + +def parse_args(): + p = argparse.ArgumentParser(description=DOC_PARAGRAPHS[0], + epilog=DOC_PARAGRAPHS[-1], + allow_abbrev=False) + p.add_argument('--package-type', metavar='TYPE', + default='package', + help='Specify the package type, e.g. "rpm" or "deb"') + p.add_argument('--package-name', metavar='NAME', + help='The name of the package (e.g. "foo" or "libbar")') + p.add_argument('--package-version', metavar='VERSION', + help='The full version of the package (e.g. 1.5-1.fc35.s390x)') + p.add_argument('--package-architecture', metavar='ARCH', + help='The code architecture of the binaries (e.g. arm64 or s390x)') + p.add_argument('--cpe', + help='NIST CPE identifier of the vendor operating system, or \'auto\' to parse from system-release-cpe or os-release') + p.add_argument('--rpm', metavar='NEVRA', + help='Extract type,name,version,architecture from a full rpm name') + p.add_argument('--debug-info-url', metavar='URL', + help='URL of the debuginfod server where sources can be queried') + p.add_argument('--readonly', metavar='BOOL', + type=str_to_bool, default=True, + help='Make the notes section read-only (requires binutils 2.38)') + p.add_argument('--root', metavar='PATH', type=Path, default="/", + help='When a file (eg: /usr/lib/os-release) is parsed, open it relatively from this hierarchy') + p.add_argument('--version', action='version', version=f'%(prog)s {__version__}') + + opts = p.parse_args() + + return opts + +def encode_bytes(arr): + return ' '.join('BYTE(0x{:02x})'.format(n) for n in arr) + +def encode_bytes_lines(arr, prefix='', label='string'): + assert len(arr) % 4 == 0 + s = bytes(arr).decode() + yield prefix + encode_bytes(arr[:4]) + ' /* {}: {!r} */'.format(label, s) + for offset in range(4, len(arr), 4): + yield prefix + encode_bytes(arr[offset:offset+4]) + +def encode_length(s, prefix='', label='string'): + n = (len(s) + 1) * 4 // 4 + return f'{prefix}LONG(0x{n:04x}) /* Length of {label} including NUL */' + +def encode_note_id(id, prefix=''): + return f'{prefix}LONG(0x{id:04x}) /* Note ID */' + +def pad_string(s): + return [0] * ((len(s) + 4) // 4 * 4 - len(s)) + +def encode_string(s, prefix='', label='string'): + arr = list(s.encode()) + pad_string(s) + yield from encode_bytes_lines(arr, prefix=prefix, label=label) + +def encode_note(note_name, note_id, owner, value, readonly=True, prefix=''): + l1 = encode_length(owner, prefix=prefix + ' ', label='Owner') + l2 = encode_length(value, prefix=prefix + ' ', label='Value') + l3 = encode_note_id(note_id, prefix=prefix + ' ') + l4 = encode_string(owner, prefix=prefix + ' ', label='Owner') + l5 = encode_string(value, prefix=prefix + ' ', label='Value') + readonly = '(READONLY) ' if readonly else '' + + return [prefix + '.note.{} {}: ALIGN(4) {{'.format(note_name, readonly), + l1, l2, l3, *l4, *l5, + prefix + '}'] + +NOTE_ID= 0xcafe1a7e + +def json_serialize(s): + # Avoid taking space in the ELF header if there's no value to store + return json.dumps({k: v for k, v in s.items() if v is not None}, + ensure_ascii=False, + separators=(',', ':')) + +def gather_data(opts): + if opts.cpe == 'auto': + try: + with open(Path(opts.root, 'usr/lib/system-release-cpe'), 'r') as f: + opts.cpe = f.read() + except FileNotFoundError: + opts.cpe = read_os_release('CPE_NAME', root=opts.root) + if opts.cpe is None or opts.cpe == "": + raise ValueError(f"Could not read {opts.root}usr/lib/system-release-cpe or CPE_NAME from {opts.root}usr/lib/os-release") + + if opts.rpm: + split = re.match(r'(.*?)-([0-9].*)\.(.*)', opts.rpm) + if not split: + raise ValueError('{!r} does not seem to be a valid package name'.format(opts.rpm)) + opts.package_type = 'rpm' + opts.package_name = split.group(1) + opts.package_version = split.group(2) + opts.package_architecture = split.group(3) + + data = { + 'type': opts.package_type, + 'name': opts.package_name, + 'version': opts.package_version, + 'architecture': opts.package_architecture, + } + if opts.cpe: + data['osCpe'] = opts.cpe + else: + data['os'] = read_os_release('ID', root=opts.root) + data['osVersion'] = read_os_release('VERSION_ID', root=opts.root) + if opts.debug_info_url: + data['debugInfoUrl'] = opts.debug_info_url + return data + +def generate_section(data, readonly=True): + json = json_serialize(data) + + section = encode_note('package', NOTE_ID, 'FDO', json, readonly=readonly, prefix=' ') + return ['SECTIONS', '{', + *section, + '}', + 'INSERT AFTER .note.gnu.build-id;', + '/* HINT: add -Wl,-dT,/path/to/this/file to $LDFLAGS */'] + +if __name__ == '__main__': + opts = parse_args() + data = gather_data(opts) + lines = generate_section(data, readonly=opts.readonly) + + print('\n'.join(lines)) diff --git a/generate-package-notes.sh b/generate-package-notes.sh new file mode 100755 index 0000000..7581091 --- /dev/null +++ b/generate-package-notes.sh @@ -0,0 +1,287 @@ +#!/bin/sh +# SPDX-License-Identifier: CC0-1.0 +# +# Generates a linker script to insert a .note.package section with a +# JSON payload. The contents are derived from the specified options and the +# os-release file. Use the output with -Wl,-dT,/path/to/output in $LDFLAGS. +# +# $ ./generate-package-notes.sh --type rpm --name systemd --version 248~rc2-1.fc34 --architecture x86_64 --osCpe 'cpe:/o:fedoraproject:fedora:33' +# SECTIONS +# { +# .note.package (READONLY) : ALIGN(4) { +# BYTE(0x04) BYTE(0x00) BYTE(0x00) BYTE(0x00) /* Length of Owner including NUL */ +# BYTE(0x7c) BYTE(0x00) BYTE(0x00) BYTE(0x00) /* Length of Value including NUL */ +# BYTE(0x7e) BYTE(0x1a) BYTE(0xfe) BYTE(0xca) /* Note ID */ +# BYTE(0x46) BYTE(0x44) BYTE(0x4f) BYTE(0x00) /* Owner: 'FDO\x00' */ +# BYTE(0x7b) BYTE(0x22) BYTE(0x74) BYTE(0x79) /* Value: '{"type":"rpm","name":"systemd","version":"248~rc2-1.fc34","architecture":"x86_64","osCpe":"cpe:/o:fedoraproject:fedora:33"}\x00' */ +# BYTE(0x70) BYTE(0x65) BYTE(0x22) BYTE(0x3a) +# BYTE(0x22) BYTE(0x72) BYTE(0x70) BYTE(0x6d) +# BYTE(0x22) BYTE(0x2c) BYTE(0x22) BYTE(0x6e) +# BYTE(0x61) BYTE(0x6d) BYTE(0x65) BYTE(0x22) +# BYTE(0x3a) BYTE(0x22) BYTE(0x73) BYTE(0x79) +# BYTE(0x73) BYTE(0x74) BYTE(0x65) BYTE(0x6d) +# BYTE(0x64) BYTE(0x22) BYTE(0x2c) BYTE(0x22) +# BYTE(0x76) BYTE(0x65) BYTE(0x72) BYTE(0x73) +# BYTE(0x69) BYTE(0x6f) BYTE(0x6e) BYTE(0x22) +# BYTE(0x3a) BYTE(0x22) BYTE(0x32) BYTE(0x34) +# BYTE(0x38) BYTE(0x7e) BYTE(0x72) BYTE(0x63) +# BYTE(0x32) BYTE(0x2d) BYTE(0x31) BYTE(0x2e) +# BYTE(0x66) BYTE(0x63) BYTE(0x33) BYTE(0x34) +# BYTE(0x22) BYTE(0x2c) BYTE(0x22) BYTE(0x61) +# BYTE(0x72) BYTE(0x63) BYTE(0x68) BYTE(0x69) +# BYTE(0x74) BYTE(0x65) BYTE(0x63) BYTE(0x74) +# BYTE(0x75) BYTE(0x72) BYTE(0x65) BYTE(0x22) +# BYTE(0x3a) BYTE(0x22) BYTE(0x78) BYTE(0x38) +# BYTE(0x36) BYTE(0x5f) BYTE(0x36) BYTE(0x34) +# BYTE(0x22) BYTE(0x2c) BYTE(0x22) BYTE(0x6f) +# BYTE(0x73) BYTE(0x43) BYTE(0x70) BYTE(0x65) +# BYTE(0x22) BYTE(0x3a) BYTE(0x22) BYTE(0x63) +# BYTE(0x70) BYTE(0x65) BYTE(0x3a) BYTE(0x2f) +# BYTE(0x6f) BYTE(0x3a) BYTE(0x66) BYTE(0x65) +# BYTE(0x64) BYTE(0x6f) BYTE(0x72) BYTE(0x61) +# BYTE(0x70) BYTE(0x72) BYTE(0x6f) BYTE(0x6a) +# BYTE(0x65) BYTE(0x63) BYTE(0x74) BYTE(0x3a) +# BYTE(0x66) BYTE(0x65) BYTE(0x64) BYTE(0x6f) +# BYTE(0x72) BYTE(0x61) BYTE(0x3a) BYTE(0x33) +# BYTE(0x33) BYTE(0x22) BYTE(0x7d) BYTE(0x00) +# } +# } +# INSERT AFTER .note.gnu.build-id; +# /* HINT: add -Wl,-dT,/path/to/this/file to $LDFLAGS */ +# +# See https://systemd.io/COREDUMP_PACKAGE_METADATA/ for details. + + +json= +readonly="(READONLY) " +root= + +help() { + echo "Usage: $0 [OPTION]..." + echo "Generate a package notes linker script from specified metadata." + echo + echo " -h, --help display this help and exit" + echo " --readonly BOOL whether to add the READONLY attribute to script (default: true)" + echo " --root PATH when a file (eg: os-release) is parsed, open it relatively to this hierarchy (default: not set)" + echo " --cpe VALUE NIST CPE identifier of the vendor operating system, or 'auto' to parse from system-release-cpe or os-release" + echo " --package-type TYPE set the package type (e.g. 'rpm' or 'deb')" + echo " --package-name NAME set the package name" + echo " --package-version VERSION set the package version" + echo " --package-architecture ARCH set the package architecture" + echo " --NAME VALUE set an arbitrary name/value pair" +} + +invalid_argument() { + printf 'ERROR: "%s" requires a non-empty option argument.\n' "${1}" >&2 + exit 1 +} + +append_parameter() { + if [ -z "${2}" ]; then + invalid_argument "${1}" + fi + + # Posix-compatible substring check + case "$json" in + *"\"${1}\":"*) echo "Duplicated argument: --${1}"; exit 1 ;; + esac + + if [ -z "${json}" ]; then + json="{\"${1}\":\"${2}\"" + else + json="${json},\"${1}\":\"${2}\"" + fi +} + +# Support the same fixed parameters as the python script +parse_options() { + cpe= + + while :; do + case $1 in + -h|-\?|--help) + help + exit + ;; + --readonly) + if [ -z "${2}" ]; then + invalid_argument "${1}" + fi + case $2 in + no|NO|No|false|FALSE|False|0) + readonly="" + ;; + esac + shift + ;; + --root) + if [ -z "${2}" ] || [ ! -d "${2}" ]; then + invalid_argument "${1}" + fi + root="${2}" + shift + ;; + --package-type) + append_parameter "type" "${2}" + shift + ;; + --package-name) + append_parameter "name" "${2}" + shift + ;; + --package-version) + append_parameter "version" "${2}" + shift + ;; + --package-architecture) + append_parameter "architecture" "${2}" + shift + ;; + --cpe) + if [ -z "${2}" ]; then + invalid_argument "${1}" + fi + cpe="${2}" + shift + ;; + --debug-info-url) + append_parameter "debugInfoUrl" "${2}" + shift + ;; + --*) + # Allow passing arbitrary name/value pairs + append_parameter "$(echo "${1}" | cut -c 3-)" "${2}" + shift + ;; + -*) + printf 'WARNING: Unknown option (ignored): %s\n' "${1}" >&2 + ;; + *) + break + esac + + shift + done + + # Parse at the end, so that --root can be used in any position + if [ "${cpe}" = "auto" ]; then + if [ -r "${root}/usr/lib/system-release-cpe" ]; then + cpe="$(cat "${root}/usr/lib/system-release-cpe")" + elif [ -r "${root}/etc/os-release" ]; then + # shellcheck disable=SC1090 disable=SC1091 + cpe="$(. "${root}/etc/os-release" && echo "${CPE_NAME}")" + elif [ -r "${root}/usr/lib/os-release" ]; then + # shellcheck disable=SC1090 disable=SC1091 + cpe="$(. "${root}/usr/lib/os-release" && echo "${CPE_NAME}")" + fi + if [ -z "${cpe}" ]; then + printf 'ERROR: --cpe auto but cannot read %s/usr/lib/system-release-cpe or parse CPE_NAME from %s/etc/os-release or %s/usr/lib/os-release.\n' "${root}" "${root}" "${root}" >&2 + exit 1 + fi + fi + if [ -n "${cpe}" ]; then + append_parameter "osCpe" "${cpe}" + fi + + # Terminate the JSON object + if [ -n "${json}" ]; then + json="${json}}" + fi + + return "$#" +} + +pad_comment() { + for _ in $(seq "$1"); do + printf '\\x00' + done +} + +pad_string() { + for i in $(seq "$1"); do + if [ $(( ( $2 + i - 1) % 4 )) -eq 0 ]; then + printf '\n%sBYTE(0x00)' "${3}" + else + printf ' BYTE(0x00)' + fi + done +} + +write_string() { + text="$1" + prefix="$2" + label="$3" + total="$4" + + # We always have at least the terminating NULL + if [ $(( total % 4)) -eq 0 ]; then + padding_nulls=1 + else + padding_nulls="$(( 1 + 4 - (total % 4) ))" + fi + + for i in $(seq ${#text}); do + if [ $(( i % 4)) -eq 1 ]; then + printf '\n%s' "$prefix" + else + printf ' ' + fi + byte=$(echo "${text}" | cut -c "${i}") + printf 'BYTE(0x%02x)' "'${byte}" + + # Print the json object as a comment after the first 4 bytes + # to match the output of the older script, including padding NUL. + if [ "${i}" -eq 4 ]; then + printf " /* %s: '%s" "$label" "$text" + pad_comment "${padding_nulls}" + printf "' */" + fi + done + + pad_string "${padding_nulls}" "${#text}" "$prefix" + printf '\n' +} + +write_script() { + # NULL terminator is included in the size, but not padding + value_len=$(( ${#1} + 1 )) + + if [ "${value_len}" -gt 65536 ]; then + printf 'ERROR: "%s" is too long.\n' "${1}" >&2 + exit 1 + fi + + printf 'SECTIONS\n{\n' + printf ' .note.package %s: ALIGN(4) {\n' "${readonly}" + # Note that for the binary fields we use the native 4 bytes type, to avoid + # endianness issues. + printf ' LONG(0x0004) /* Length of Owner including NUL */\n' + printf ' LONG(0x%04x) /* Length of Value including NUL */\n' \ + ${value_len} + printf ' LONG(0xcafe1a7e) /* Note ID */\n' + + printf " BYTE(0x46) BYTE(0x44) BYTE(0x4f) BYTE(0x00) /* Owner: 'FDO\\\\x00' */" # newline will be added by write_string + + write_string "$1" ' ' 'Value' "$value_len" + + printf ' }\n}\n' + printf 'INSERT AFTER .note.gnu.build-id;\n' + # shellcheck disable=SC2016 + printf '/* HINT: add -Wl,-dT,/path/to/this/file to $LDFLAGS */\n' +} + +if ! parse_options "$@" && [ "$#" -gt 0 ]; then + # Not supported on every distro + if [ -r "${root}/usr/lib/system-release-cpe" ]; then + cpe="$(cat "${root}/usr/lib/system-release-cpe")" + json_cpe=",\"osCpe\":\"${cpe}\"" + fi + + # old-style invocation with positional parameters for backward compatibility + json="$(printf '{"type":"rpm","name":"%s","version":"%s","architecture":"%s"%s}' "$1" "$2" "$3" "$json_cpe")" +elif [ -z "${json}" ]; then + help + exit 1 +fi + +write_script "$json" diff --git a/hello.spec b/hello.spec new file mode 100644 index 0000000..eb0025d --- /dev/null +++ b/hello.spec @@ -0,0 +1,85 @@ +# 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 +BuildRequires: gcc +BuildRequires: rpmdevtools +BuildRequires: python3 +BuildRequires: python3-simplejson + +Source0: generate-package-notes.py + +%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 <libhello.c +const char* greeting(void) { + return "Hello"; +} +EOF +cat <hello.c +#include +extern char* greeting(void); +int main() { + puts(greeting()); + return 0; +} +EOF + +%build +set -eo pipefail + +ld_version="$(ld --version | sed -r -n '1 {s/.*version (.*)/\1/p}')" +set +e +rpmdev-vercmp "$ld_version" "2.38" >/dev/null +if [ $? == 12 ]; then + readonly="--readonly=no" +fi +set -e + +%if %{with notes} +python3 %{SOURCE0} $readonly --rpm '%{name}-%{VERSION}-%{RELEASE}.%{_arch}' | \ + tee notes.ld +%endif + +LDFLAGS="%{build_ldflags} %{?with_notes:-Wl,-dT,$PWD/notes.ld}" +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 - 0-1 +- Test diff --git a/man/generate-package-notes.1 b/man/generate-package-notes.1 new file mode 100644 index 0000000..ebdc55a --- /dev/null +++ b/man/generate-package-notes.1 @@ -0,0 +1,136 @@ +.TH GENERATE\-PACKAGE\-NOTES 1 "May 2021" +.SH NAME +generate\-package\-notes \- generate a linker script for package metadata +.SH SYNOPSIS +.B generate\-package\-notes +.RI [ OPTION ...] +.SH DESCRIPTION +ELF binaries get stamped with a unique, build-time generated hex string +identifier called build-id, which gets embedded as an ELF note called +.IR \%.note.gnu.build\-id . +In most cases, this allows to associate a stripped binary with +its debugging information. +It is used, for example, to dynamically fetch DWARF symbols from +a debuginfo server, or to query the local package manager and find out +the package metadata or, again, the DWARF symbols or program sources. +.PP +However, this usage of the build-id requires either local metadata, +usually set up by the package manager, +or access to a remote server over the network. +Both of those might be unavailable or forbidden. +.PP +Thus it becomes desirable to add additional metadata to a binary +at build time, so that +.BR \%systemd\-coredump (8) +and other services analyzing +core files are able to extract said metadata simply from the core file +itself, without external dependencies. +The metadata is embedded in a single ELF header section, +in a key-value JSON format. +.PP +The metadata format is intentionally left open, +so that vendors can add their own information. +A set of well-known keys is defined in the document +.UR https://systemd.io/COREDUMP_PACKAGE_METADATA/ +.B Package Metadata for Core Files +.UE , +and hopefully shared among all vendors. +.PP +.B generate\-package\-notes +generates a linker script on standard output, +which can then be used at build time via +.I \%LDFLAGS="\-Wl,\-dT,/path/to/generated/script" +to include the note in the binary. +If a Debian package is built using the +.BR dh (1) +sequencer, the generation can be partly automated using +.BR \%dh_package_notes (1). +.SH OPTIONS +.TP +.BI \-\-package\-type= TYPE +Set the key +.I type +to +.IR TYPE . +This defaults to +.IR package , +but for Debian packages it should be set to +.IR deb . +.TP +.BI \-\-package\-name= NAME +Set the key +.I name +to +.IR NAME . +This defaults to the empty string, but for Debian packages it should +be set to the name of the binary package containing the binary. +.TP +.BI \-\-package\-version= VERSION +Set the key +.I version +to +.IR VERSION . +This defaults to the empty string, but for Debian packages it should +be set to the Debian version of the binary package containing the binary. +.TP +.BI \-\-package\-architecture= ARCHITECTURE +Set the key +.I architecture +to +.IR ARCHITECTURE . +This defaults to the empty string, but for Debian packages it should +be set to the Debian architecture identifier of the binary. +.TP +.TP +.BI \-\-root= PATH +When reading files, for example /usr/lib/system-release-cpe and /usr/lib/os-release, +open them relatively to the specified directory. +.TP +.BI \-\-cpe= CPE +Set the key +.I osCpe +to +.IR CPE . +If the special value +.I auto +is passed, then the content of the /usr/lib/system-release-cpe file, or the value of CPE_NAME +from os-release if the former was not found, will be used. +.TP +.BI \-\-rpm= PACKAGE \- VERSION +Set the keys +.I type +to +.BR rpm , +.I name +to +.IR PACKAGE , +and +.I version +to +.IR VERSION . +Overrides +.BR \-\-package\-type , +.BR \-\-package\-name , +.BR \-\-package\-architecture , +and +.BR \-\-package\-version . +.TP +.BI \-\-debug\-info\-url= URL +Set the key +.I debugInfoUrl +to +.IR URL . +By default this key is omitted, but for Debian packages it should +be set to the official Debian debuginfod server address +.IR https://debuginfod.debian.org/ . +.TP +.BR \-h ", " \-\-help +Show a short help message and exit. +.SH SEE ALSO +.ad l +.nh +.BR dh_package_notes (1), +.BR systemd\-coredump (8), +.UR https://systemd.io/COREDUMP_PACKAGE_METADATA/ +.B Package Metadata for Core Files +.UE diff --git a/rpm/macros.package-notes-srpm b/rpm/macros.package-notes-srpm new file mode 100644 index 0000000..7899ba6 --- /dev/null +++ b/rpm/macros.package-notes-srpm @@ -0,0 +1,57 @@ +# 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_file +# +# The other macros can be undefined too to replace parts of the +# functionality. If %_generate_package_note_file is undefined, the +# linker script will not be generated, but the link flags may still +# refer to it. This may be useful if the default generation method is +# insufficient and a different mechanism will be used to generate +# %_package_note_file. If %_package_note_flags is undefined, the +# linker argument that injects the script will not be added to +# %build_ldfags, but the linker script would still be generated. + +# The name of the file with the linker script. If %{buildsubdir} is +# defined, the file will be placed therein. Otherwise, one level up, +# directly in %{_builddir}. +# +# Note that %{version}-%{release} used here might be redefined from +# the "primary" values when subpackages with different version-release +# are specified. The contents of the script use the shell variable +# $RPM_PACKAGE_NAME, $RPM_PACKAGE_VERSION, $RPM_PACKAGE_RELEASE, +# and $RPM_ARCH that are set early and seem to always contain the "primary" +# values for the main package. +%_package_note_file %{_builddir}%{?buildsubdir:/%{buildsubdir}}/.package_note-%{name}-%{version}-%{release}.%{_arch}.ld + +# Which linker will be used? This should be either "bfd", "gold", or +# "lld". Unfortunately linkers other than bfd do not support some of +# the options that we'd like to use, so if this is set to anything +# other than "bfd", note insertion is disabled. +# +# (The default linker for clang on armv7hl is lld.) +%_package_note_linker %["%_target_cpu" == "armv7hl" && "%{toolchain}" == "clang" ? "lld" : "bfd"] + +# Whether to specify the READONLY attribute for the inserted +# section. We generally want this, but binutils <= 2.37 and other +# linkers do not support it. +%_package_note_readonly %["%_package_note_linker" == "bfd"?"1":"0"] + +# Overall status: 1 if looks like we can insert the note, 0 otherwise +%_package_note_status %[0%{?_package_note_file:1} && 0%{?name:1} && "%_target_cpu" != "noarch" && "%_package_note_linker" == "bfd" ? 1 : 0] + +# The linker flags to be passed to the compiler to insert the notes section. +%_package_note_flags %[%_package_note_status?"-Wl,%["%_package_note_linker" != "lld"?"-dT":"-T"],%{_package_note_file}":""] + +# The command to actually generate the linker script that inserts the +# notes file. This command is automatically used as part of the build +# preamble. +%_generate_package_note_file %[%_package_note_status?"if [ -f %{_rpmconfigdir}/generate-package-notes.sh ]; then %{_rpmconfigdir}/generate-package-notes.sh %[0%{?_package_note_readonly}?"":"--readonly no "]--package-name ${RPM_PACKAGE_NAME:?} --package-version ${RPM_PACKAGE_VERSION:?}-${RPM_PACKAGE_RELEASE:?} --package-architecture ${RPM_ARCH:?} --cpe auto >%{_package_note_file}; fi":""] diff --git a/tests/_generate_package_notes.py b/tests/_generate_package_notes.py new file mode 120000 index 0000000..e37242a --- /dev/null +++ b/tests/_generate_package_notes.py @@ -0,0 +1 @@ +../generate-package-notes.py \ No newline at end of file diff --git a/tests/resources/__init__.py b/tests/resources/__init__.py new file mode 100644 index 0000000..19b869e --- /dev/null +++ b/tests/resources/__init__.py @@ -0,0 +1 @@ +# SPDX-License-Identifier: CC0-1.0 diff --git a/tests/resources/fedora-cpe-os-release.ld b/tests/resources/fedora-cpe-os-release.ld new file mode 100644 index 0000000..37e67b2 --- /dev/null +++ b/tests/resources/fedora-cpe-os-release.ld @@ -0,0 +1,25 @@ +SECTIONS +{ + .note.package (READONLY) : ALIGN(4) { + LONG(0x0004) /* Length of Owner including NUL */ + LONG(0x0038) /* Length of Value including NUL */ + LONG(0xcafe1a7e) /* Note ID */ + BYTE(0x46) BYTE(0x44) BYTE(0x4f) BYTE(0x00) /* Owner: 'FDO\x00' */ + BYTE(0x7b) BYTE(0x22) BYTE(0x74) BYTE(0x79) /* Value: '{"type":"rpm","osCpe":"cpe:/o:fedoraproject:fedora:34"}\x00' */ + BYTE(0x70) BYTE(0x65) BYTE(0x22) BYTE(0x3a) + BYTE(0x22) BYTE(0x72) BYTE(0x70) BYTE(0x6d) + BYTE(0x22) BYTE(0x2c) BYTE(0x22) BYTE(0x6f) + BYTE(0x73) BYTE(0x43) BYTE(0x70) BYTE(0x65) + BYTE(0x22) BYTE(0x3a) BYTE(0x22) BYTE(0x63) + BYTE(0x70) BYTE(0x65) BYTE(0x3a) BYTE(0x2f) + BYTE(0x6f) BYTE(0x3a) BYTE(0x66) BYTE(0x65) + BYTE(0x64) BYTE(0x6f) BYTE(0x72) BYTE(0x61) + BYTE(0x70) BYTE(0x72) BYTE(0x6f) BYTE(0x6a) + BYTE(0x65) BYTE(0x63) BYTE(0x74) BYTE(0x3a) + BYTE(0x66) BYTE(0x65) BYTE(0x64) BYTE(0x6f) + BYTE(0x72) BYTE(0x61) BYTE(0x3a) BYTE(0x33) + BYTE(0x34) BYTE(0x22) BYTE(0x7d) BYTE(0x00) + } +} +INSERT AFTER .note.gnu.build-id; +/* HINT: add -Wl,-dT,/path/to/this/file to $LDFLAGS */ diff --git a/tests/resources/fedora-cpe-system-release.ld b/tests/resources/fedora-cpe-system-release.ld new file mode 100644 index 0000000..c11f5fa --- /dev/null +++ b/tests/resources/fedora-cpe-system-release.ld @@ -0,0 +1,25 @@ +SECTIONS +{ + .note.package (READONLY) : ALIGN(4) { + LONG(0x0004) /* Length of Owner including NUL */ + LONG(0x0038) /* Length of Value including NUL */ + LONG(0xcafe1a7e) /* Note ID */ + BYTE(0x46) BYTE(0x44) BYTE(0x4f) BYTE(0x00) /* Owner: 'FDO\x00' */ + BYTE(0x7b) BYTE(0x22) BYTE(0x74) BYTE(0x79) /* Value: '{"type":"rpm","osCpe":"cpe:/o:fedoraproject:fedora:33"}\x00' */ + BYTE(0x70) BYTE(0x65) BYTE(0x22) BYTE(0x3a) + BYTE(0x22) BYTE(0x72) BYTE(0x70) BYTE(0x6d) + BYTE(0x22) BYTE(0x2c) BYTE(0x22) BYTE(0x6f) + BYTE(0x73) BYTE(0x43) BYTE(0x70) BYTE(0x65) + BYTE(0x22) BYTE(0x3a) BYTE(0x22) BYTE(0x63) + BYTE(0x70) BYTE(0x65) BYTE(0x3a) BYTE(0x2f) + BYTE(0x6f) BYTE(0x3a) BYTE(0x66) BYTE(0x65) + BYTE(0x64) BYTE(0x6f) BYTE(0x72) BYTE(0x61) + BYTE(0x70) BYTE(0x72) BYTE(0x6f) BYTE(0x6a) + BYTE(0x65) BYTE(0x63) BYTE(0x74) BYTE(0x3a) + BYTE(0x66) BYTE(0x65) BYTE(0x64) BYTE(0x6f) + BYTE(0x72) BYTE(0x61) BYTE(0x3a) BYTE(0x33) + BYTE(0x33) BYTE(0x22) BYTE(0x7d) BYTE(0x00) + } +} +INSERT AFTER .note.gnu.build-id; +/* HINT: add -Wl,-dT,/path/to/this/file to $LDFLAGS */ diff --git a/tests/resources/fedora-long-name.ld b/tests/resources/fedora-long-name.ld new file mode 100644 index 0000000..122eefa --- /dev/null +++ b/tests/resources/fedora-long-name.ld @@ -0,0 +1,88 @@ +SECTIONS +{ + .note.package (READONLY) : ALIGN(4) { + LONG(0x0004) /* Length of Owner including NUL */ + LONG(0x0133) /* Length of Value including NUL */ + LONG(0xcafe1a7e) /* Note ID */ + BYTE(0x46) BYTE(0x44) BYTE(0x4f) BYTE(0x00) /* Owner: 'FDO\x00' */ + BYTE(0x7b) BYTE(0x22) BYTE(0x74) BYTE(0x79) /* Value: '{"type":"rpm","name":"rust-plist+enable_unstable_features_that_may_break_with_minor_version_bumps-devel","version":"200:1.3.1~rc1.post2^final3","architecture":"ppc64le","osCpe":"cpe:/o:fedoraproject:fedora:35","debugInfoUrl":"https://somewhere.on.the.internet.there.is.a.server.which.is.never.wrong/query"}\x00\x00' */ + BYTE(0x70) BYTE(0x65) BYTE(0x22) BYTE(0x3a) + BYTE(0x22) BYTE(0x72) BYTE(0x70) BYTE(0x6d) + BYTE(0x22) BYTE(0x2c) BYTE(0x22) BYTE(0x6e) + BYTE(0x61) BYTE(0x6d) BYTE(0x65) BYTE(0x22) + BYTE(0x3a) BYTE(0x22) BYTE(0x72) BYTE(0x75) + BYTE(0x73) BYTE(0x74) BYTE(0x2d) BYTE(0x70) + BYTE(0x6c) BYTE(0x69) BYTE(0x73) BYTE(0x74) + BYTE(0x2b) BYTE(0x65) BYTE(0x6e) BYTE(0x61) + BYTE(0x62) BYTE(0x6c) BYTE(0x65) BYTE(0x5f) + BYTE(0x75) BYTE(0x6e) BYTE(0x73) BYTE(0x74) + BYTE(0x61) BYTE(0x62) BYTE(0x6c) BYTE(0x65) + BYTE(0x5f) BYTE(0x66) BYTE(0x65) BYTE(0x61) + BYTE(0x74) BYTE(0x75) BYTE(0x72) BYTE(0x65) + BYTE(0x73) BYTE(0x5f) BYTE(0x74) BYTE(0x68) + BYTE(0x61) BYTE(0x74) BYTE(0x5f) BYTE(0x6d) + BYTE(0x61) BYTE(0x79) BYTE(0x5f) BYTE(0x62) + BYTE(0x72) BYTE(0x65) BYTE(0x61) BYTE(0x6b) + BYTE(0x5f) BYTE(0x77) BYTE(0x69) BYTE(0x74) + BYTE(0x68) BYTE(0x5f) BYTE(0x6d) BYTE(0x69) + BYTE(0x6e) BYTE(0x6f) BYTE(0x72) BYTE(0x5f) + BYTE(0x76) BYTE(0x65) BYTE(0x72) BYTE(0x73) + BYTE(0x69) BYTE(0x6f) BYTE(0x6e) BYTE(0x5f) + BYTE(0x62) BYTE(0x75) BYTE(0x6d) BYTE(0x70) + BYTE(0x73) BYTE(0x2d) BYTE(0x64) BYTE(0x65) + BYTE(0x76) BYTE(0x65) BYTE(0x6c) BYTE(0x22) + BYTE(0x2c) BYTE(0x22) BYTE(0x76) BYTE(0x65) + BYTE(0x72) BYTE(0x73) BYTE(0x69) BYTE(0x6f) + BYTE(0x6e) BYTE(0x22) BYTE(0x3a) BYTE(0x22) + BYTE(0x32) BYTE(0x30) BYTE(0x30) BYTE(0x3a) + BYTE(0x31) BYTE(0x2e) BYTE(0x33) BYTE(0x2e) + BYTE(0x31) BYTE(0x7e) BYTE(0x72) BYTE(0x63) + BYTE(0x31) BYTE(0x2e) BYTE(0x70) BYTE(0x6f) + BYTE(0x73) BYTE(0x74) BYTE(0x32) BYTE(0x5e) + BYTE(0x66) BYTE(0x69) BYTE(0x6e) BYTE(0x61) + BYTE(0x6c) BYTE(0x33) BYTE(0x22) BYTE(0x2c) + BYTE(0x22) BYTE(0x61) BYTE(0x72) BYTE(0x63) + BYTE(0x68) BYTE(0x69) BYTE(0x74) BYTE(0x65) + BYTE(0x63) BYTE(0x74) BYTE(0x75) BYTE(0x72) + BYTE(0x65) BYTE(0x22) BYTE(0x3a) BYTE(0x22) + BYTE(0x70) BYTE(0x70) BYTE(0x63) BYTE(0x36) + BYTE(0x34) BYTE(0x6c) BYTE(0x65) BYTE(0x22) + BYTE(0x2c) BYTE(0x22) BYTE(0x6f) BYTE(0x73) + BYTE(0x43) BYTE(0x70) BYTE(0x65) BYTE(0x22) + BYTE(0x3a) BYTE(0x22) BYTE(0x63) BYTE(0x70) + BYTE(0x65) BYTE(0x3a) BYTE(0x2f) BYTE(0x6f) + BYTE(0x3a) BYTE(0x66) BYTE(0x65) BYTE(0x64) + BYTE(0x6f) BYTE(0x72) BYTE(0x61) BYTE(0x70) + BYTE(0x72) BYTE(0x6f) BYTE(0x6a) BYTE(0x65) + BYTE(0x63) BYTE(0x74) BYTE(0x3a) BYTE(0x66) + BYTE(0x65) BYTE(0x64) BYTE(0x6f) BYTE(0x72) + BYTE(0x61) BYTE(0x3a) BYTE(0x33) BYTE(0x35) + BYTE(0x22) BYTE(0x2c) BYTE(0x22) BYTE(0x64) + BYTE(0x65) BYTE(0x62) BYTE(0x75) BYTE(0x67) + BYTE(0x49) BYTE(0x6e) BYTE(0x66) BYTE(0x6f) + BYTE(0x55) BYTE(0x72) BYTE(0x6c) BYTE(0x22) + BYTE(0x3a) BYTE(0x22) BYTE(0x68) BYTE(0x74) + BYTE(0x74) BYTE(0x70) BYTE(0x73) BYTE(0x3a) + BYTE(0x2f) BYTE(0x2f) BYTE(0x73) BYTE(0x6f) + BYTE(0x6d) BYTE(0x65) BYTE(0x77) BYTE(0x68) + BYTE(0x65) BYTE(0x72) BYTE(0x65) BYTE(0x2e) + BYTE(0x6f) BYTE(0x6e) BYTE(0x2e) BYTE(0x74) + BYTE(0x68) BYTE(0x65) BYTE(0x2e) BYTE(0x69) + BYTE(0x6e) BYTE(0x74) BYTE(0x65) BYTE(0x72) + BYTE(0x6e) BYTE(0x65) BYTE(0x74) BYTE(0x2e) + BYTE(0x74) BYTE(0x68) BYTE(0x65) BYTE(0x72) + BYTE(0x65) BYTE(0x2e) BYTE(0x69) BYTE(0x73) + BYTE(0x2e) BYTE(0x61) BYTE(0x2e) BYTE(0x73) + BYTE(0x65) BYTE(0x72) BYTE(0x76) BYTE(0x65) + BYTE(0x72) BYTE(0x2e) BYTE(0x77) BYTE(0x68) + BYTE(0x69) BYTE(0x63) BYTE(0x68) BYTE(0x2e) + BYTE(0x69) BYTE(0x73) BYTE(0x2e) BYTE(0x6e) + BYTE(0x65) BYTE(0x76) BYTE(0x65) BYTE(0x72) + BYTE(0x2e) BYTE(0x77) BYTE(0x72) BYTE(0x6f) + BYTE(0x6e) BYTE(0x67) BYTE(0x2f) BYTE(0x71) + BYTE(0x75) BYTE(0x65) BYTE(0x72) BYTE(0x79) + BYTE(0x22) BYTE(0x7d) BYTE(0x00) BYTE(0x00) + } +} +INSERT AFTER .note.gnu.build-id; +/* HINT: add -Wl,-dT,/path/to/this/file to $LDFLAGS */ diff --git a/tests/resources/fedora-package.ld b/tests/resources/fedora-package.ld new file mode 100644 index 0000000..c90ab05 --- /dev/null +++ b/tests/resources/fedora-package.ld @@ -0,0 +1,33 @@ +SECTIONS +{ + .note.package (READONLY) : ALIGN(4) { + LONG(0x0004) /* Length of Owner including NUL */ + LONG(0x0058) /* Length of Value including NUL */ + LONG(0xcafe1a7e) /* Note ID */ + BYTE(0x46) BYTE(0x44) BYTE(0x4f) BYTE(0x00) /* Owner: 'FDO\x00' */ + BYTE(0x7b) BYTE(0x22) BYTE(0x74) BYTE(0x79) /* Value: '{"type":"rpm","name":"package","version":"1.2.3","architecture":"noarch","osCpe":"CPE"}\x00' */ + BYTE(0x70) BYTE(0x65) BYTE(0x22) BYTE(0x3a) + BYTE(0x22) BYTE(0x72) BYTE(0x70) BYTE(0x6d) + BYTE(0x22) BYTE(0x2c) BYTE(0x22) BYTE(0x6e) + BYTE(0x61) BYTE(0x6d) BYTE(0x65) BYTE(0x22) + BYTE(0x3a) BYTE(0x22) BYTE(0x70) BYTE(0x61) + BYTE(0x63) BYTE(0x6b) BYTE(0x61) BYTE(0x67) + BYTE(0x65) BYTE(0x22) BYTE(0x2c) BYTE(0x22) + BYTE(0x76) BYTE(0x65) BYTE(0x72) BYTE(0x73) + BYTE(0x69) BYTE(0x6f) BYTE(0x6e) BYTE(0x22) + BYTE(0x3a) BYTE(0x22) BYTE(0x31) BYTE(0x2e) + BYTE(0x32) BYTE(0x2e) BYTE(0x33) BYTE(0x22) + BYTE(0x2c) BYTE(0x22) BYTE(0x61) BYTE(0x72) + BYTE(0x63) BYTE(0x68) BYTE(0x69) BYTE(0x74) + BYTE(0x65) BYTE(0x63) BYTE(0x74) BYTE(0x75) + BYTE(0x72) BYTE(0x65) BYTE(0x22) BYTE(0x3a) + BYTE(0x22) BYTE(0x6e) BYTE(0x6f) BYTE(0x61) + BYTE(0x72) BYTE(0x63) BYTE(0x68) BYTE(0x22) + BYTE(0x2c) BYTE(0x22) BYTE(0x6f) BYTE(0x73) + BYTE(0x43) BYTE(0x70) BYTE(0x65) BYTE(0x22) + BYTE(0x3a) BYTE(0x22) BYTE(0x43) BYTE(0x50) + BYTE(0x45) BYTE(0x22) BYTE(0x7d) BYTE(0x00) + } +} +INSERT AFTER .note.gnu.build-id; +/* HINT: add -Wl,-dT,/path/to/this/file to $LDFLAGS */ diff --git a/tests/resources/root-no-cpe/usr/lib/os-release b/tests/resources/root-no-cpe/usr/lib/os-release new file mode 100644 index 0000000..86268aa --- /dev/null +++ b/tests/resources/root-no-cpe/usr/lib/os-release @@ -0,0 +1 @@ +CPE_NAME="cpe:/o:fedoraproject:fedora:34" diff --git a/tests/resources/root/usr/lib/os-release b/tests/resources/root/usr/lib/os-release new file mode 100644 index 0000000..560da14 --- /dev/null +++ b/tests/resources/root/usr/lib/os-release @@ -0,0 +1 @@ +ID=fedora diff --git a/tests/resources/root/usr/lib/system-release-cpe b/tests/resources/root/usr/lib/system-release-cpe new file mode 100644 index 0000000..769fe43 --- /dev/null +++ b/tests/resources/root/usr/lib/system-release-cpe @@ -0,0 +1 @@ +cpe:/o:fedoraproject:fedora:33 \ No newline at end of file diff --git a/tests/resources/very-short-rw.ld b/tests/resources/very-short-rw.ld new file mode 100644 index 0000000..01a1f0f --- /dev/null +++ b/tests/resources/very-short-rw.ld @@ -0,0 +1,29 @@ +SECTIONS +{ + .note.package : ALIGN(4) { + LONG(0x0004) /* Length of Owner including NUL */ + LONG(0x0047) /* Length of Value including NUL */ + LONG(0xcafe1a7e) /* Note ID */ + BYTE(0x46) BYTE(0x44) BYTE(0x4f) BYTE(0x00) /* Owner: 'FDO\x00' */ + BYTE(0x7b) BYTE(0x22) BYTE(0x74) BYTE(0x79) /* Value: '{"type":"deb","name":"A","version":"0","architecture":"x","osCpe":"o"}\x00\x00' */ + BYTE(0x70) BYTE(0x65) BYTE(0x22) BYTE(0x3a) + BYTE(0x22) BYTE(0x64) BYTE(0x65) BYTE(0x62) + BYTE(0x22) BYTE(0x2c) BYTE(0x22) BYTE(0x6e) + BYTE(0x61) BYTE(0x6d) BYTE(0x65) BYTE(0x22) + BYTE(0x3a) BYTE(0x22) BYTE(0x41) BYTE(0x22) + BYTE(0x2c) BYTE(0x22) BYTE(0x76) BYTE(0x65) + BYTE(0x72) BYTE(0x73) BYTE(0x69) BYTE(0x6f) + BYTE(0x6e) BYTE(0x22) BYTE(0x3a) BYTE(0x22) + BYTE(0x30) BYTE(0x22) BYTE(0x2c) BYTE(0x22) + BYTE(0x61) BYTE(0x72) BYTE(0x63) BYTE(0x68) + BYTE(0x69) BYTE(0x74) BYTE(0x65) BYTE(0x63) + BYTE(0x74) BYTE(0x75) BYTE(0x72) BYTE(0x65) + BYTE(0x22) BYTE(0x3a) BYTE(0x22) BYTE(0x78) + BYTE(0x22) BYTE(0x2c) BYTE(0x22) BYTE(0x6f) + BYTE(0x73) BYTE(0x43) BYTE(0x70) BYTE(0x65) + BYTE(0x22) BYTE(0x3a) BYTE(0x22) BYTE(0x6f) + BYTE(0x22) BYTE(0x7d) BYTE(0x00) BYTE(0x00) + } +} +INSERT AFTER .note.gnu.build-id; +/* HINT: add -Wl,-dT,/path/to/this/file to $LDFLAGS */ diff --git a/tests/resources/very-short.ld b/tests/resources/very-short.ld new file mode 100644 index 0000000..5a3b9eb --- /dev/null +++ b/tests/resources/very-short.ld @@ -0,0 +1,29 @@ +SECTIONS +{ + .note.package (READONLY) : ALIGN(4) { + LONG(0x0004) /* Length of Owner including NUL */ + LONG(0x0047) /* Length of Value including NUL */ + LONG(0xcafe1a7e) /* Note ID */ + BYTE(0x46) BYTE(0x44) BYTE(0x4f) BYTE(0x00) /* Owner: 'FDO\x00' */ + BYTE(0x7b) BYTE(0x22) BYTE(0x74) BYTE(0x79) /* Value: '{"type":"deb","name":"A","version":"0","architecture":"x","osCpe":"o"}\x00\x00' */ + BYTE(0x70) BYTE(0x65) BYTE(0x22) BYTE(0x3a) + BYTE(0x22) BYTE(0x64) BYTE(0x65) BYTE(0x62) + BYTE(0x22) BYTE(0x2c) BYTE(0x22) BYTE(0x6e) + BYTE(0x61) BYTE(0x6d) BYTE(0x65) BYTE(0x22) + BYTE(0x3a) BYTE(0x22) BYTE(0x41) BYTE(0x22) + BYTE(0x2c) BYTE(0x22) BYTE(0x76) BYTE(0x65) + BYTE(0x72) BYTE(0x73) BYTE(0x69) BYTE(0x6f) + BYTE(0x6e) BYTE(0x22) BYTE(0x3a) BYTE(0x22) + BYTE(0x30) BYTE(0x22) BYTE(0x2c) BYTE(0x22) + BYTE(0x61) BYTE(0x72) BYTE(0x63) BYTE(0x68) + BYTE(0x69) BYTE(0x74) BYTE(0x65) BYTE(0x63) + BYTE(0x74) BYTE(0x75) BYTE(0x72) BYTE(0x65) + BYTE(0x22) BYTE(0x3a) BYTE(0x22) BYTE(0x78) + BYTE(0x22) BYTE(0x2c) BYTE(0x22) BYTE(0x6f) + BYTE(0x73) BYTE(0x43) BYTE(0x70) BYTE(0x65) + BYTE(0x22) BYTE(0x3a) BYTE(0x22) BYTE(0x6f) + BYTE(0x22) BYTE(0x7d) BYTE(0x00) BYTE(0x00) + } +} +INSERT AFTER .note.gnu.build-id; +/* HINT: add -Wl,-dT,/path/to/this/file to $LDFLAGS */ diff --git a/tests/test_basics.py b/tests/test_basics.py new file mode 100644 index 0000000..550e7ec --- /dev/null +++ b/tests/test_basics.py @@ -0,0 +1,54 @@ +# SPDX-License-Identifier: CC0-1.0 + +import sys +from importlib import resources +from pathlib import Path + +from _generate_package_notes import gather_data, generate_section + + +class dict_dot(dict): + __getattr__ = dict.get + +def test_fedora_package(): + input = dict(type='rpm', name='package', version='1.2.3', architecture='noarch', osCpe='CPE') + text = '\n'.join(generate_section(input)) + expected = resources.read_text('resources', 'fedora-package.ld') + assert text == expected[:-1] + +def test_very_short(): + input = dict(type='deb', name='A', version='0', architecture='x', osCpe='o') + text = '\n'.join(generate_section(input)) + expected = resources.read_text('resources', 'very-short.ld') + assert text == expected[:-1] + +def test_very_short_rw(): + input = dict(type='deb', name='A', version='0', architecture='x', osCpe='o') + text = '\n'.join(generate_section(input, readonly=False)) + expected = resources.read_text('resources', 'very-short-rw.ld') + assert text == expected[:-1] + +def test_fedora_long_name(): + input = dict(type='rpm', + name='rust-plist+enable_unstable_features_that_may_break_with_minor_version_bumps-devel', + version='200:1.3.1~rc1.post2^final3', + architecture='ppc64le', + osCpe='cpe:/o:fedoraproject:fedora:35', + debugInfoUrl='https://somewhere.on.the.internet.there.is.a.server.which.is.never.wrong/query') + text = '\n'.join(generate_section(input)) + expected = resources.read_text('resources', 'fedora-long-name.ld') + assert text == expected[:-1] + +def test_auto_cpe_system_release(): + opts = dict_dot(package_type='rpm', cpe='auto', root=Path(__file__).absolute().parent / 'resources/root/') + input = gather_data(opts) + text = '\n'.join(generate_section(input)) + expected = resources.read_text('resources', 'fedora-cpe-system-release.ld') + assert text == expected[:-1] + +def test_auto_cpe_os_release(): + opts = dict_dot(package_type='rpm', cpe='auto', root=Path(__file__).absolute().parent / 'resources/root-no-cpe/') + input = gather_data(opts) + text = '\n'.join(generate_section(input)) + expected = resources.read_text('resources', 'fedora-cpe-os-release.ld') + assert text == expected[:-1] diff --git a/tests/test_basics.sh b/tests/test_basics.sh new file mode 100755 index 0000000..8262cca --- /dev/null +++ b/tests/test_basics.sh @@ -0,0 +1,13 @@ +#!/bin/bash +# SPDX-License-Identifier: CC0-1.0 + +set -e + +testdir="$(dirname "$(realpath "${0}")")" + +diff <("${testdir}/../generate-package-notes.sh" --type rpm --name package --version 1.2.3 --architecture noarch --osCpe CPE) "${testdir}/resources/fedora-package.ld" +diff <("${testdir}/../generate-package-notes.sh" --type deb --name A --package-version 0 --architecture x --cpe o) "${testdir}/resources/very-short.ld" +diff <("${testdir}/../generate-package-notes.sh" --type deb --name A --version 0 --architecture x --osCpe o --readonly false) "${testdir}/resources/very-short-rw.ld" +diff <("${testdir}/../generate-package-notes.sh" --type rpm --name rust-plist+enable_unstable_features_that_may_break_with_minor_version_bumps-devel --version 200:1.3.1~rc1.post2^final3 --architecture ppc64le --osCpe cpe:/o:fedoraproject:fedora:35 --debugInfoUrl https://somewhere.on.the.internet.there.is.a.server.which.is.never.wrong/query) "${testdir}/resources/fedora-long-name.ld" +diff <("${testdir}/../generate-package-notes.sh" --type rpm --cpe auto --root "${testdir}/resources/root-no-cpe/") "${testdir}/resources/fedora-cpe-os-release.ld" +diff <("${testdir}/../generate-package-notes.sh" --type rpm --cpe auto --root "${testdir}/resources/root/") "${testdir}/resources/fedora-cpe-system-release.ld"