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))