From df086b5ae58fc1ec89e8f08a5601944b873e6a71 Mon Sep 17 00:00:00 2001 From: Omair Majid Date: Thu, 29 Oct 2026 13:20:53 -0400 Subject: [PATCH] Extend system and bundled native library tests Add two new tests 1. Test system library declarationso n packages Ensure that, for every library in the VMR, we explicitly bundle it or use the system copy. This means that a new library being introduced, or an existing library being removed upstream would be caught by this and require the package to mark it as bundled or system. Discover native library candidates from the VMR release branch matching the runtime being tested. For each candidate, check direct ELF dependencies resolved outside the .NET installation and bundled provides in installed .NET RPMs. Skip known header-only (eg rapidjson) libraries and non-library directories (eg patches). 2. Verify that all the expected system-libraries are correct Check the expected libraries, including brotli, zstd, and zlib are all linked against the system. Assisted-by: GPT-6.1 Terra --- .github/workflows/ci.yml | 8 +- README.md | 4 +- .../test.json | 8 +- required-system-libraries/test.sh | 94 +++++++ system-openssl/test.sh | 26 -- system-or-bundled-libraries/test.json | 15 ++ system-or-bundled-libraries/test.sh | 252 ++++++++++++++++++ 7 files changed, 372 insertions(+), 35 deletions(-) rename {system-openssl => required-system-libraries}/test.json (51%) create mode 100755 required-system-libraries/test.sh delete mode 100755 system-openssl/test.sh create mode 100644 system-or-bundled-libraries/test.json create mode 100755 system-or-bundled-libraries/test.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ea70491f..a3251151 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,12 +47,16 @@ jobs: # with use_preview_copr: true. When a version ships officially, move dotnet_version # into the main list above and remove its include entries (and this block if empty). include: + - container_image: registry.fedoraproject.org/fedora:43 + dotnet_version: "11.0" - container_image: registry.fedoraproject.org/fedora:44 dotnet_version: "11.0" - use_preview_copr: true + - container_image: registry.fedoraproject.org/fedora:45 + dotnet_version: "11.0" - container_image: registry.fedoraproject.org/fedora:rawhide dotnet_version: "11.0" - use_preview_copr: true + - container_image: registry.access.redhat.com/ubi8 + dotnet_version: "11.0" container: image: ${{ matrix.container_image }} diff --git a/README.md b/README.md index 2d52f208..a2382c02 100644 --- a/README.md +++ b/README.md @@ -12,5 +12,5 @@ $ git clone https://github.com/redhat-developer/dotnet-bunny && cd dotnet-bunny ### Dependencies -Dependencies(apk): babeltrace bash bash-completion binutils coreutils curl file findutils g++ jq libstdc++-dev lldb lttng-ust lttng-tools npm postgresql psqlodbc python3 sed strace unixodbc wget zlib-dev -Dependencies(dnf): awk babeltrace bash-completion bc curl findutils gcc-c++ jq libstdc++-devel lttng-ust lttng-tools npm postgresql-odbc postgresql-server python3 strace unixODBC /usr/bin/file /usr/bin/free /usr/bin/lldb /usr/bin/readelf /usr/bin/su which wget zlib-devel +Dependencies(apk): babeltrace bash bash-completion binutils coreutils curl file findutils git g++ jq libstdc++-dev lldb lttng-ust lttng-tools npm postgresql psqlodbc python3 sed strace unixodbc wget zlib-dev +Dependencies(dnf): awk babeltrace bash-completion bc curl findutils gcc-c++ git-core jq libstdc++-devel lttng-ust lttng-tools npm postgresql-odbc postgresql-server python3 strace unixODBC /usr/bin/file /usr/bin/free /usr/bin/lldb /usr/bin/readelf /usr/bin/su which wget zlib-devel diff --git a/system-openssl/test.json b/required-system-libraries/test.json similarity index 51% rename from system-openssl/test.json rename to required-system-libraries/test.json index ba31f7fb..bdc0617c 100644 --- a/system-openssl/test.json +++ b/required-system-libraries/test.json @@ -1,14 +1,12 @@ { - "name": "system-openssl", + "name": "required-system-libraries", "enabled": true, "requiresSdk": false, - "version": "2.1", + "version": "11.0", "versionSpecific": false, "type": "bash", "cleanup": true, "skipWhen": [ - "portable", // portable dlopen()s system openssl - ], - "ignoredRIDs":[ + "portable" // portable builds dlopen OpenSSL ] } diff --git a/required-system-libraries/test.sh b/required-system-libraries/test.sh new file mode 100755 index 00000000..5beee772 --- /dev/null +++ b/required-system-libraries/test.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash + +# Required system libraries +# +# This test checks the selected runtime's native libraries for direct ELF +# dependencies on system copies. +# +# These are the third-party libraries expected to be **directly linked** by +# these runtime binaries; platform libraries such as libc and the C++ runtime +# are not an exhaustive portability policy. + +set -euo pipefail +IFS=$'\n\t' + +framework_dir=$(../dotnet-directory --framework "$1") +dotnet_dir=$(realpath "$(../dotnet-directory)") +failed=0 + +check_dependency() { + local file="$framework_dir/$1" library="$2" needed soname dependencies path + if [[ ! -f "$file" ]]; then + echo "FAIL: missing $file" + failed=1 + return + fi + + # Check direct ELF dependencies, not just ldd's transitive dependencies: a + # transitive dependency does not prove that .NET uses the system library. + if ! needed=$(readelf -d "$file"); then + echo "FAIL: cannot read dependencies of $file" + failed=1 + return + fi + # Match the complete SONAME, including a possible ABI version. + soname=$(printf '%s\n' "$needed" | awk -v lib="$library" ' + /\(NEEDED\)/ { + name = $NF + gsub(/[][]/, "", name) + if (name == lib ".so" || index(name, lib ".so.") == 1) print name + }') + if [[ -z "$soname" || "$soname" == *$'\n'* ]]; then + echo "FAIL: $file must directly depend on $library.so (found: ${soname:-none})" + failed=1 + return + fi + if ! dependencies=$(ldd "$file"); then + echo "FAIL: ldd failed for $file" + failed=1 + return + fi + path=$(printf '%s\n' "$dependencies" | awk -v name="$soname" '$1 == name && $2 == "=>" {print $3}') + if [[ "$path" != /* || ! -e "$path" ]]; then + echo "FAIL: $soname is not resolved for $file" + failed=1 + return + fi + path=$(realpath "$path") + if [[ "$path" != /usr/* ]]; then + echo "FAIL: $soname resolves outside /usr: $path" + failed=1 + return + fi + if [[ "$path" == "$dotnet_dir" || "$path" == "$dotnet_dir/"* ]]; then + echo "FAIL: $soname resolves inside the .NET installation: $path" + failed=1 + return + fi + echo "PASS: $file directly links to system $soname ($path)" +} + +# Source-built runtime dependencies which must not be statically bundled. +# brotli +check_dependency libSystem.IO.Compression.Native.so libbrotlidec +check_dependency libSystem.IO.Compression.Native.so libbrotlienc + +# openssl +check_dependency libSystem.Security.Cryptography.Native.OpenSsl.so libcrypto +check_dependency libSystem.Security.Cryptography.Native.OpenSsl.so libssl + +# zlib +check_dependency libSystem.IO.Compression.Native.so libz + +# zstd +# Zstandard support was added in .NET 11. Compare major versions so that +# preview versions and future releases are covered too. +major_version=${1%%[!0-9]*} +if [[ -z "$major_version" ]]; then + echo "FAIL: cannot determine major version from $1" + failed=1 +elif (( 10#$major_version >= 11 )); then + check_dependency libSystem.IO.Compression.Native.so libzstd +fi + +exit "$failed" diff --git a/system-openssl/test.sh b/system-openssl/test.sh deleted file mode 100755 index 78e9a3b9..00000000 --- a/system-openssl/test.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env bash - -# Make sure .NET has ldd-visible links to OpenSSL. We prefer that over -# using OpenSSL via dlopen (which is more likely to fail at runtime). - -set -euo pipefail -IFS=$'\n\t' -set -x - -dotnet_dir="$(../dotnet-directory)" - -find_ssl_args=("-regex" '.*System\.Security\.Cryptography\.Native\.OpenSsl\.so$') - -# print for debugging -find "${dotnet_dir}" \ - "${find_ssl_args[@]}" - -find "${dotnet_dir}" \ - "${find_ssl_args[@]}" \ - -exec ldd {} \; \ - | grep -E '(libcrypto|libssl)' - -find "${dotnet_dir}" \ - "${find_ssl_args[@]}" \ - -exec nm -D {} \; \ - | grep SSL diff --git a/system-or-bundled-libraries/test.json b/system-or-bundled-libraries/test.json new file mode 100644 index 00000000..1d1fbd52 --- /dev/null +++ b/system-or-bundled-libraries/test.json @@ -0,0 +1,15 @@ +{ + "name": "system-or-bundled-libraries", + "enabled": true, + "requiresSdk": true, + "version": "11.0", + "versionSpecific": false, + "type": "bash", + "cleanup": true, + "skipWhen": [ + "os=alpine", // uses RPM specific packages + "vmr-ci" // uses packages + ], + "ignoredRIDs":[ + ] +} diff --git a/system-or-bundled-libraries/test.sh b/system-or-bundled-libraries/test.sh new file mode 100755 index 00000000..fa7f363b --- /dev/null +++ b/system-or-bundled-libraries/test.sh @@ -0,0 +1,252 @@ +#!/usr/bin/env bash + +# Check that libraries are declared correctly as bundled or are system-linked +# +# If a library must be system-linked, add it to required-system-libraries as +# well. + +set -euo pipefail +IFS=$'\n\t' + +fail() { + echo "FAIL: $*" >&2 + exit 1 +} + +discover_vmr_libraries() { + local external_path=src/runtime/src/native/external + + echo "Cloning https://github.com/dotnet/dotnet.git at $branch" + git clone --quiet --depth 1 --filter=blob:none --sparse --branch "$branch" \ + https://github.com/dotnet/dotnet.git "$vmr" + + git -C "$vmr" cat-file -e "HEAD:$external_path" + git -C "$vmr" ls-tree "HEAD:$external_path" > "vmr-tree" + awk '$1 == "040000" { print $4 }' "vmr-tree" > "vmr-libraries" + mapfile -t vmr_libraries < "vmr-libraries" + ((${#vmr_libraries[@]} > 0)) || fail "no library directories found in $branch:$external_path" + echo "VMR libraries (${#vmr_libraries[@]}):" + printf ' - %s\n' "${vmr_libraries[@]}" +} + +collect_bundled_libraries() { + local package capability + local -a packages + + rpm -qa --qf '%{NAME}\n' | awk -v version="${major}.${minor}" ' + $0 == "dotnet-host" || + $0 == "aspnetcore-runtime-" version || + $0 == "dotnet-runtime-" version || + $0 == "dotnet-sdk-" version || + $0 == "dotnet-sdk-aot-" version + ' | sort -u > "dotnet-packages" + mapfile -t packages < "dotnet-packages" + + printf 'RPM packages checked (%d):\n' "${#packages[@]}" + if ((${#packages[@]} > 0)); then + printf ' - %s\n' "${packages[@]}" + else + echo " (none)" + fi + + : > "rpm-provides" + for package in "${packages[@]}"; do + rpm -q --provides "$package" | awk -v package="$package" ' + $1 ~ /^bundled\(/ { print package "\t" $1 } + ' >> "rpm-provides" + done + while IFS=$'\t' read -r package capability; do + bundled_packages["$capability"]=$package + done < "rpm-provides" +} + +collect_system_libraries() { + local file soname dependency_path resolved ldd_status + + # Write the list first so failures from find or file cannot be hidden by a + # process substitution. Exclude common managed assemblies and metadata. + find -L "$root" -type f \ + ! -iname '*.dll' ! -iname '*.pdb' ! -iname '*.xml' ! -iname '*.json' \ + ! -iname '*.txt' ! -iname '*.targets' ! -iname '*.globalconfig' \ + ! -iname '*.props' ! -iname '*.pubxml' ! -iname '*.nupkg' ! -iname '*.xaml' \ + -exec sh -c ' + for file do + description=$(file -b "$file") || exit 1 + case "$description" in + *ELF*) printf "%s\0" "$file" ;; + esac + done + ' _ {} + > "elf-files" + + : > "system-dependencies" + while IFS= read -r -d '' file; do + ((elf_count += 1)) + if ! LC_ALL=C readelf -d "$file" > "readelf-output" 2>&1; then + cat "readelf-output" >&2 + fail "could not inspect ELF dependencies: $file" + fi + awk '$2 == "(NEEDED)" { + gsub(/^\[|\]$/, "", $NF) + print $NF + }' "readelf-output" > "direct-sonames" + + # Static and relocatable ELF files do not need dynamic resolution. + [[ -s direct-sonames ]] || continue + ldd_status=0 + LC_ALL=C ldd "$file" > "ldd-output" 2>&1 || ldd_status=$? + if awk '$2 == "=>" && $3 == "not" && $4 == "found" { found = 1 } + END { exit !found }' "ldd-output"; then + cat "ldd-output" >&2 + fail "missing ELF dependencies: $file" + fi + if ((ldd_status != 0)); then + echo "WARNING: could not resolve ELF dependencies: $file" >&2 + cat "ldd-output" >&2 + continue + fi + # ldd also lists transitive dependencies; only retain direct DT_NEEDED entries. + awk ' + NR == FNR { direct[$0] = 1; next } + $2 == "=>" && $1 in direct && $3 ~ /^\// { print $1 "\t" $3 } + ' "direct-sonames" "ldd-output" > "resolved-direct" + while IFS=$'\t' read -r soname dependency_path; do + resolved=$(readlink -f "$dependency_path" 2>/dev/null) || continue + [[ -e $resolved ]] || continue + [[ $resolved != "$root"/* ]] && printf '%s\t%s\t%s\n' "$soname" "$resolved" "$file" >> "system-dependencies" + done < "resolved-direct" + done < "elf-files" + ((elf_count > 0)) || fail "no ELF files found under .NET installation root $root" +} + +check_library_evidence() { + local library=$1 skip_reason canonical_library pattern + local capability bundled_evidence system_count + + # TODO: Verify skipped VMR directories remain header-only or non-library. + case $library in + patches) skip_reason='patches' ;; + rapidjson) skip_reason='header-only library' ;; + libunwind_extras) skip_reason='build configuration for libunwind' ;; + *) skip_reason='' ;; + esac + if [[ -n $skip_reason ]]; then + echo "SKIP: $library ($skip_reason)" + ((skipped += 1)) + return + fi + + # zlib-intel is an optimized zlib implementation, not a separate library. + canonical_library=$library + [[ $library == zlib-intel ]] && canonical_library=zlib + pattern=${soname_patterns[$canonical_library]-} + if [[ -z $pattern ]]; then + echo "FAIL: $library (no SONAME mapping configured)" >&2 + ((failed += 1)) + return + fi + + # ENVIRON preserves the backslashes in the Bash SONAME patterns. + SONAME_PATTERN="^${pattern}$" awk -F '\t' ' + $1 ~ ENVIRON["SONAME_PATTERN"] && !seen[$3]++ { + printf " %s: %s => %s\n", $3, $1, $2 + } + ' "system-dependencies" > "library-evidence" + + capability="bundled($canonical_library)" + bundled_evidence=${bundled_packages[$capability]-} + if [[ -s library-evidence && -n $bundled_evidence ]]; then + echo "FAIL: $library (both direct system dependency and bundled RPM declaration found in $bundled_evidence)" >&2 + cat "library-evidence" >&2 + ((failed += 1)) + elif [[ -s library-evidence ]]; then + system_count=$(wc -l < "library-evidence") + echo "PASS: $library (direct system dependency found in $system_count ELF files)" + cat "library-evidence" + ((passed += 1)) + elif [[ -n $bundled_evidence ]]; then + echo "PASS: $library (bundled by $bundled_evidence)" + ((passed += 1)) + else + echo "FAIL: $library (no direct system dependency or bundled RPM declaration found)" >&2 + ((failed += 1)) + fi +} + +check_unexpected_bundled_libraries() { + local library capability + local -A vmr_capabilities=() + + for library in "${vmr_libraries[@]}"; do + vmr_capabilities["bundled($library)"]=1 + # Match the alias used when checking evidence for zlib-intel. + if [[ $library == zlib-intel ]]; then + vmr_capabilities['bundled(zlib)']=1 + fi + done + + for capability in "${!bundled_packages[@]}"; do + if [[ -z ${vmr_capabilities[$capability]-} ]]; then + echo "FAIL: $capability (declared by ${bundled_packages[$capability]}, but not found in VMR branch $branch)" >&2 + ((unexpected_bundled += 1)) + fi + done +} + +sdk_version=$(dotnet --version) || fail "could not get SDK version from dotnet on PATH" +version_without_suffix=${sdk_version%%-*} +if [[ ! $version_without_suffix =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then + fail "cannot map SDK version '$sdk_version' to a VMR release branch" +fi +major=${BASH_REMATCH[1]} +minor=${BASH_REMATCH[2]} +branch="release/${major}.0.1xx" + +command -v rpm >/dev/null || fail "rpm tooling is required to inspect installed package metadata" +command -v file >/dev/null || fail "file utility is required to identify ELF files" +command -v readelf >/dev/null || fail "readelf is required to inspect direct ELF dependencies" +command -v ldd >/dev/null || fail "ldd is required to resolve ELF dependencies" + +root=$(../dotnet-directory --root) || fail "could not determine .NET installation root" +root=$(realpath "$root") +[[ -d $root ]] || fail "canonical .NET installation root does not exist: $root" + +# Map VMR dependency directory names explicitly to their ELF SONAMEs. Keep +# these keys aligned with src/runtime/src/native/external on active branches. +architecture=$(uname -m) +declare -A soname_patterns=( + [LinuxTracepoints]='libtracepoint\.so([.][0-9]+)*' + [brotli]='libbrotli(common|dec|enc)\.so([.][0-9]+)*' + [libunwind]="libunwind(-${architecture})?\\.so([.][0-9]+)*" + [llvm-libunwind]="libunwind(-${architecture})?\\.so([.][0-9]+)*" + [zlib]='lib(z|z-ng)\.so([.][0-9]+)*' + [zlib-ng]='lib(z|z-ng)\.so([.][0-9]+)*' + [zstd]='libzstd\.so([.][0-9]+)*' +) + +workdir=$(mktemp -d "./vmr-work.XXXXXXXX") || fail "could not create working directory" +trap 'rm -rf -- "$workdir"' EXIT +vmr="$workdir/vmr" +declare -a vmr_libraries=() +declare -A bundled_packages=() +elf_count=0 +passed=0 +failed=0 +skipped=0 +unexpected_bundled=0 + +discover_vmr_libraries +collect_bundled_libraries +collect_system_libraries +for library in "${vmr_libraries[@]}"; do + check_library_evidence "$library" +done +check_unexpected_bundled_libraries + +if ((failed > 0 || unexpected_bundled > 0)); then + echo "Diagnostics:" >&2 + printf ' .NET root: %s\n' "$root" >&2 + printf ' ELF files inspected: %d\n' "$elf_count" >&2 +fi + +echo "Summary: $passed passed, $failed failed, $skipped skipped (${#vmr_libraries[@]} VMR libraries); $unexpected_bundled unexpected bundled declarations (SDK $sdk_version, branch $branch)" +((failed == 0 && unexpected_bundled == 0))