1#!/bin/bash 2# SPDX-License-Identifier: GPL-2.0-only 3# Script to check commits for UAPI backwards compatibility 4 5set -o errexit 6set -o pipefail 7 8print_usage() { 9 name=$(basename "$0") 10 cat << EOF 11$name - check for UAPI header stability across Git commits 12 13By default, the script will check to make sure the latest commit (or current 14dirty changes) did not introduce ABI changes when compared to HEAD^1. You can 15check against additional commit ranges with the -b and -p options. 16 17The script will not check UAPI headers for architectures other than the one 18defined in ARCH. 19 20Usage: $name [-b BASE_REF] [-p PAST_REF] [-j N] [-l ERROR_LOG] [-i] [-q] [-v] 21 22Options: 23 -b BASE_REF Base git reference to use for comparison. If unspecified or empty, 24 will use any dirty changes in tree to UAPI files. If there are no 25 dirty changes, HEAD will be used. 26 -p PAST_REF Compare BASE_REF to PAST_REF (e.g. -p v6.1). If unspecified or empty, 27 will use BASE_REF^1. Must be an ancestor of BASE_REF. Only headers 28 that exist on PAST_REF will be checked for compatibility. 29 -j JOBS Number of checks to run in parallel (default: number of CPU cores). 30 -l ERROR_LOG Write error log to file (default: no error log is generated). 31 -i Ignore ambiguous changes that may or may not break UAPI compatibility. 32 -q Quiet operation. 33 -v Verbose operation (print more information about each header being checked). 34 35Environmental args: 36 ABIDIFF Custom path to abidiff binary 37 CROSS_COMPILE Toolchain prefix for compiler 38 CC C compiler (default is "\${CROSS_COMPILE}gcc") 39 ARCH Target architecture for the UAPI check (default is host arch) 40 41Exit codes: 42 $SUCCESS) Success 43 $FAIL_ABI) ABI difference detected 44 $FAIL_PREREQ) Prerequisite not met 45EOF 46} 47 48readonly SUCCESS=0 49readonly FAIL_ABI=1 50readonly FAIL_PREREQ=2 51 52# Print to stderr 53eprintf() { 54 # shellcheck disable=SC2059 55 printf "$@" >&2 56} 57 58# Expand an array with a specific character (similar to Python string.join()) 59join() { 60 local IFS="$1" 61 shift 62 printf "%s" "$*" 63} 64 65# Create abidiff suppressions 66gen_suppressions() { 67 # Common enum variant names which we don't want to worry about 68 # being shifted when new variants are added. 69 local -a enum_regex=( 70 ".*_AFTER_LAST$" 71 ".*_CNT$" 72 ".*_COUNT$" 73 ".*_END$" 74 ".*_LAST$" 75 ".*_MASK$" 76 ".*_MAX$" 77 ".*_MAX_BIT$" 78 ".*_MAX_BPF_ATTACH_TYPE$" 79 ".*_MAX_ID$" 80 ".*_MAX_SHIFT$" 81 ".*_NBITS$" 82 ".*_NETDEV_NUMHOOKS$" 83 ".*_NFT_META_IIFTYPE$" 84 ".*_NL80211_ATTR$" 85 ".*_NLDEV_NUM_OPS$" 86 ".*_NUM$" 87 ".*_NUM_ELEMS$" 88 ".*_NUM_IRQS$" 89 ".*_SIZE$" 90 ".*_TLSMAX$" 91 "^MAX_.*" 92 "^NUM_.*" 93 ) 94 95 # Common padding field names which can be expanded into 96 # without worrying about users. 97 local -a padding_regex=( 98 ".*end$" 99 ".*pad$" 100 ".*pad[0-9]?$" 101 ".*pad_[0-9]?$" 102 ".*padding$" 103 ".*padding[0-9]?$" 104 ".*padding_[0-9]?$" 105 ".*res$" 106 ".*resv$" 107 ".*resv[0-9]?$" 108 ".*resv_[0-9]?$" 109 ".*reserved$" 110 ".*reserved[0-9]?$" 111 ".*reserved_[0-9]?$" 112 ".*rsvd[0-9]?$" 113 ".*unused$" 114 ) 115 116 cat << EOF 117[suppress_type] 118 type_kind = enum 119 changed_enumerators_regexp = $(join , "${enum_regex[@]}") 120EOF 121 122 for p in "${padding_regex[@]}"; do 123 cat << EOF 124[suppress_type] 125 type_kind = struct 126 has_data_member_inserted_at = offset_of_first_data_member_regexp(${p}) 127EOF 128 done 129 130if [ "$IGNORE_AMBIGUOUS_CHANGES" = "true" ]; then 131 cat << EOF 132[suppress_type] 133 type_kind = struct 134 has_data_member_inserted_at = end 135 has_size_change = yes 136EOF 137fi 138} 139 140# Check if git tree is dirty 141tree_is_dirty() { 142 ! git diff --quiet 143} 144 145# Get list of files installed in $ref 146get_file_list() { 147 local -r ref="$1" 148 local -r tree="$(get_header_tree "$ref")" 149 150 # Print all installed headers, filtering out ones that can't be compiled 151 find "$tree" -type f -name '*.h' -printf '%P\n' | grep -v -f "$INCOMPAT_LIST" 152} 153 154# Add to the list of incompatible headers 155add_to_incompat_list() { 156 local -r ref="$1" 157 158 # Start with the usr/include/Makefile to get a list of the headers 159 # that don't compile using this method. 160 if [ ! -f usr/include/Makefile ]; then 161 eprintf "error - no usr/include/Makefile present at %s\n" "$ref" 162 eprintf "Note: usr/include/Makefile was added in the v5.3 kernel release\n" 163 exit "$FAIL_PREREQ" 164 fi 165 { 166 # shellcheck disable=SC2016 167 printf 'all: ; @echo $(no-header-test)\n' 168 cat usr/include/Makefile 169 } | SRCARCH="$ARCH" make --always-make -f - | tr " " "\n" \ 170 | grep -v "asm-generic" >> "$INCOMPAT_LIST" 171 172 # The makefile also skips all asm-generic files, but prints "asm-generic/%" 173 # which won't work for our grep match. Instead, print something grep will match. 174 printf "asm-generic/.*\.h\n" >> "$INCOMPAT_LIST" 175} 176 177# Compile the simple test app 178do_compile() { 179 local -r inc_dir="$1" 180 local -r header="$2" 181 local -r out="$3" 182 printf "int f(void) { return 0; }\n" | \ 183 "$CC" \ 184 -shared \ 185 -nostdlib \ 186 -fPIC \ 187 -o "$out" \ 188 -x c \ 189 -O0 \ 190 -std=c90 \ 191 -fno-eliminate-unused-debug-types \ 192 -g \ 193 "-I${inc_dir}" \ 194 "-Iusr/dummy-include" \ 195 -include "$header" \ 196 - 197} 198 199# Run make headers_install 200run_make_headers_install() { 201 local -r ref="$1" 202 local -r install_dir="$(get_header_tree "$ref")" 203 make -j "$MAX_THREADS" CROSS_COMPILE="${CROSS_COMPILE}" ARCH="$ARCH" INSTALL_HDR_PATH="$install_dir" \ 204 headers_install > /dev/null 205} 206 207# Install headers for both git refs 208install_headers() { 209 local -r base_ref="$1" 210 local -r past_ref="$2" 211 212 for ref in "$base_ref" "$past_ref"; do 213 printf "Installing user-facing UAPI headers from %s... " "${ref:-dirty tree}" 214 if [ -n "$ref" ]; then 215 git archive --format=tar --prefix="${ref}-archive/" "$ref" \ 216 | (cd "$TMP_DIR" && tar xf -) 217 ( 218 cd "${TMP_DIR}/${ref}-archive" 219 run_make_headers_install "$ref" 220 add_to_incompat_list "$ref" "$INCOMPAT_LIST" 221 ) 222 else 223 run_make_headers_install "$ref" 224 add_to_incompat_list "$ref" "$INCOMPAT_LIST" 225 fi 226 printf "OK\n" 227 done 228 sort -u -o "$INCOMPAT_LIST" "$INCOMPAT_LIST" 229 sed -i -e '/^$/d' "$INCOMPAT_LIST" 230} 231 232# Print the path to the headers_install tree for a given ref 233get_header_tree() { 234 local -r ref="$1" 235 printf "%s" "${TMP_DIR}/${ref}/usr" 236} 237 238# Check file list for UAPI compatibility 239check_uapi_files() { 240 local -r base_ref="$1" 241 local -r past_ref="$2" 242 local -r abi_error_log="$3" 243 244 local passed=0; 245 local failed=0; 246 local -a threads=() 247 set -o errexit 248 249 printf "Checking changes to UAPI headers between %s and %s...\n" "$past_ref" "${base_ref:-dirty tree}" 250 # Loop over all UAPI headers that were installed by $past_ref (if they only exist on $base_ref, 251 # there's no way they're broken and no way to compare anyway) 252 while read -r file; do 253 if [ "${#threads[@]}" -ge "$MAX_THREADS" ]; then 254 if wait "${threads[0]}"; then 255 passed=$((passed + 1)) 256 else 257 failed=$((failed + 1)) 258 fi 259 threads=("${threads[@]:1}") 260 fi 261 262 check_individual_file "$base_ref" "$past_ref" "$file" & 263 threads+=("$!") 264 done < <(get_file_list "$past_ref") 265 266 for t in "${threads[@]}"; do 267 if wait "$t"; then 268 passed=$((passed + 1)) 269 else 270 failed=$((failed + 1)) 271 fi 272 done 273 274 if [ -n "$abi_error_log" ]; then 275 printf 'Generated by "%s %s" from git ref %s\n\n' \ 276 "$0" "$*" "$(git rev-parse HEAD)" > "$abi_error_log" 277 fi 278 279 while read -r error_file; do 280 { 281 cat "$error_file" 282 printf "\n\n" 283 } | tee -a "${abi_error_log:-/dev/null}" >&2 284 done < <(find "$TMP_DIR" -type f -name '*.error' | sort) 285 286 total="$((passed + failed))" 287 if [ "$failed" -gt 0 ]; then 288 eprintf "error - %d/%d UAPI headers compatible with %s appear _not_ to be backwards compatible\n" \ 289 "$failed" "$total" "$ARCH" 290 if [ -n "$abi_error_log" ]; then 291 eprintf "Failure summary saved to %s\n" "$abi_error_log" 292 fi 293 else 294 printf "All %d UAPI headers compatible with %s appear to be backwards compatible\n" \ 295 "$total" "$ARCH" 296 fi 297 298 return "$failed" 299} 300 301# Check an individual file for UAPI compatibility 302check_individual_file() { 303 local -r base_ref="$1" 304 local -r past_ref="$2" 305 local -r file="$3" 306 307 local -r base_header="$(get_header_tree "$base_ref")/${file}" 308 local -r past_header="$(get_header_tree "$past_ref")/${file}" 309 310 if [ ! -f "$base_header" ]; then 311 mkdir -p "$(dirname "$base_header")" 312 printf "==== UAPI header %s was removed between %s and %s ====" \ 313 "$file" "$past_ref" "$base_ref" \ 314 > "${base_header}.error" 315 return 1 316 fi 317 318 compare_abi "$file" "$base_header" "$past_header" "$base_ref" "$past_ref" 319} 320 321# Perform the A/B compilation and compare output ABI 322compare_abi() { 323 local -r file="$1" 324 local -r base_header="$2" 325 local -r past_header="$3" 326 local -r base_ref="$4" 327 local -r past_ref="$5" 328 local -r log="${TMP_DIR}/log/${file}.log" 329 local -r error_log="${TMP_DIR}/log/${file}.error" 330 331 mkdir -p "$(dirname "$log")" 332 333 if ! do_compile "$(get_header_tree "$base_ref")/include" "$base_header" "${base_header}.bin" 2> "$log"; then 334 { 335 warn_str=$(printf "==== Could not compile version of UAPI header %s at %s ====\n" \ 336 "$file" "$base_ref") 337 printf "%s\n" "$warn_str" 338 cat "$log" 339 printf -- "=%.0s" $(seq 0 ${#warn_str}) 340 } > "$error_log" 341 return 1 342 fi 343 344 if ! do_compile "$(get_header_tree "$past_ref")/include" "$past_header" "${past_header}.bin" 2> "$log"; then 345 { 346 warn_str=$(printf "==== Could not compile version of UAPI header %s at %s ====\n" \ 347 "$file" "$past_ref") 348 printf "%s\n" "$warn_str" 349 cat "$log" 350 printf -- "=%.0s" $(seq 0 ${#warn_str}) 351 } > "$error_log" 352 return 1 353 fi 354 355 local ret=0 356 "$ABIDIFF" --non-reachable-types \ 357 --suppressions "$SUPPRESSIONS" \ 358 "${past_header}.bin" "${base_header}.bin" > "$log" || ret="$?" 359 if [ "$ret" -eq 0 ]; then 360 if [ "$VERBOSE" = "true" ]; then 361 printf "No ABI differences detected in %s from %s -> %s\n" \ 362 "$file" "$past_ref" "$base_ref" 363 fi 364 else 365 # Bits in abidiff's return code can be used to determine the type of error 366 if [ $((ret & 0x2)) -gt 0 ]; then 367 eprintf "error - abidiff did not run properly\n" 368 exit 1 369 fi 370 371 if [ "$IGNORE_AMBIGUOUS_CHANGES" = "true" ] && [ "$ret" -eq 4 ]; then 372 return 0 373 fi 374 375 # If the only changes were additions (not modifications to existing APIs), then 376 # there's no problem. Ignore these diffs. 377 if grep "Unreachable types summary" "$log" | grep -q "0 removed" && 378 grep "Unreachable types summary" "$log" | grep -q "0 changed"; then 379 return 0 380 fi 381 382 { 383 warn_str=$(printf "==== ABI differences detected in %s from %s -> %s ====" \ 384 "$file" "$past_ref" "$base_ref") 385 printf "%s\n" "$warn_str" 386 sed -e '/summary:/d' -e '/changed type/d' -e '/^$/d' -e 's/^/ /g' "$log" 387 printf -- "=%.0s" $(seq 0 ${#warn_str}) 388 if cmp "$past_header" "$base_header" > /dev/null 2>&1; then 389 printf "\n%s did not change between %s and %s...\n" "$file" "$past_ref" "${base_ref:-dirty tree}" 390 printf "It's possible a change to one of the headers it includes caused this error:\n" 391 grep '^#include' "$base_header" 392 printf "\n" 393 fi 394 } > "$error_log" 395 396 return 1 397 fi 398} 399 400# Check that a minimum software version number is satisfied 401min_version_is_satisfied() { 402 local -r min_version="$1" 403 local -r version_installed="$2" 404 405 printf "%s\n%s\n" "$min_version" "$version_installed" \ 406 | sort -Vc > /dev/null 2>&1 407} 408 409# Make sure we have the tools we need and the arguments make sense 410check_deps() { 411 ABIDIFF="${ABIDIFF:-abidiff}" 412 CC="${CC:-${CROSS_COMPILE}gcc}" 413 ARCH="${ARCH:-$(uname -m)}" 414 if [ "$ARCH" = "x86_64" ]; then 415 ARCH="x86" 416 fi 417 418 local -r abidiff_min_version="2.4" 419 local -r libdw_min_version_if_clang="0.171" 420 421 if ! command -v "$ABIDIFF" > /dev/null 2>&1; then 422 eprintf "error - abidiff not found!\n" 423 eprintf "Please install abigail-tools version %s or greater\n" "$abidiff_min_version" 424 eprintf "See: https://sourceware.org/libabigail/manual/libabigail-overview.html\n" 425 return 1 426 fi 427 428 local -r abidiff_version="$("$ABIDIFF" --version | cut -d ' ' -f 2)" 429 if ! min_version_is_satisfied "$abidiff_min_version" "$abidiff_version"; then 430 eprintf "error - abidiff version too old: %s\n" "$abidiff_version" 431 eprintf "Please install abigail-tools version %s or greater\n" "$abidiff_min_version" 432 eprintf "See: https://sourceware.org/libabigail/manual/libabigail-overview.html\n" 433 return 1 434 fi 435 436 if ! command -v "$CC" > /dev/null 2>&1; then 437 eprintf 'error - %s not found\n' "$CC" 438 return 1 439 fi 440 441 if "$CC" --version | grep -q clang; then 442 local -r libdw_version="$(ldconfig -v 2>/dev/null | grep -v SKIPPED | grep -m 1 -o 'libdw-[0-9]\+.[0-9]\+' | cut -c 7-)" 443 if ! min_version_is_satisfied "$libdw_min_version_if_clang" "$libdw_version"; then 444 eprintf "error - libdw version too old for use with clang: %s\n" "$libdw_version" 445 eprintf "Please install libdw from elfutils version %s or greater\n" "$libdw_min_version_if_clang" 446 eprintf "See: https://sourceware.org/elfutils/\n" 447 return 1 448 fi 449 fi 450 451 if [ ! -d "arch/${ARCH}" ]; then 452 eprintf 'error - ARCH "%s" is not a subdirectory under arch/\n' "$ARCH" 453 eprintf "Please set ARCH to one of:\n%s\n" "$(find arch -maxdepth 1 -mindepth 1 -type d -printf '%f ' | fmt)" 454 return 1 455 fi 456 457 if ! git rev-parse --is-inside-work-tree > /dev/null 2>&1; then 458 eprintf "error - this script requires the kernel tree to be initialized with Git\n" 459 return 1 460 fi 461 462 if ! git rev-parse --verify "$past_ref" > /dev/null 2>&1; then 463 printf 'error - invalid git reference "%s"\n' "$past_ref" 464 return 1 465 fi 466 467 if [ -n "$base_ref" ]; then 468 if ! git merge-base --is-ancestor "$past_ref" "$base_ref" > /dev/null 2>&1; then 469 printf 'error - "%s" is not an ancestor of base ref "%s"\n' "$past_ref" "$base_ref" 470 return 1 471 fi 472 if [ "$(git rev-parse "$base_ref")" = "$(git rev-parse "$past_ref")" ]; then 473 printf 'error - "%s" and "%s" are the same reference\n' "$past_ref" "$base_ref" 474 return 1 475 fi 476 fi 477} 478 479run() { 480 local base_ref="$1" 481 local past_ref="$2" 482 local abi_error_log="$3" 483 shift 3 484 485 if [ -z "$KERNEL_SRC" ]; then 486 KERNEL_SRC="$(realpath "$(dirname "$0")"/..)" 487 fi 488 489 cd "$KERNEL_SRC" 490 491 if [ -z "$base_ref" ] && ! tree_is_dirty; then 492 base_ref=HEAD 493 fi 494 495 if [ -z "$past_ref" ]; then 496 if [ -n "$base_ref" ]; then 497 past_ref="${base_ref}^1" 498 else 499 past_ref=HEAD 500 fi 501 fi 502 503 if ! check_deps; then 504 exit "$FAIL_PREREQ" 505 fi 506 507 TMP_DIR=$(mktemp -d) 508 readonly TMP_DIR 509 trap 'rm -rf "$TMP_DIR"' EXIT 510 511 readonly INCOMPAT_LIST="${TMP_DIR}/incompat_list.txt" 512 touch "$INCOMPAT_LIST" 513 514 readonly SUPPRESSIONS="${TMP_DIR}/suppressions.txt" 515 gen_suppressions > "$SUPPRESSIONS" 516 517 # Run make install_headers for both refs 518 install_headers "$base_ref" "$past_ref" 519 520 # Check for any differences in the installed header trees 521 if diff -r -q "$(get_header_tree "$base_ref")" "$(get_header_tree "$past_ref")" > /dev/null 2>&1; then 522 printf "No changes to UAPI headers were applied between %s and %s\n" "$past_ref" "${base_ref:-dirty tree}" 523 exit "$SUCCESS" 524 fi 525 526 if ! check_uapi_files "$base_ref" "$past_ref" "$abi_error_log"; then 527 exit "$FAIL_ABI" 528 fi 529} 530 531main() { 532 MAX_THREADS=$(nproc) 533 VERBOSE="false" 534 IGNORE_AMBIGUOUS_CHANGES="false" 535 quiet="false" 536 local base_ref="" 537 while getopts "hb:p:j:l:iqv" opt; do 538 case $opt in 539 h) 540 print_usage 541 exit "$SUCCESS" 542 ;; 543 b) 544 base_ref="$OPTARG" 545 ;; 546 p) 547 past_ref="$OPTARG" 548 ;; 549 j) 550 MAX_THREADS="$OPTARG" 551 ;; 552 l) 553 abi_error_log="$OPTARG" 554 ;; 555 i) 556 IGNORE_AMBIGUOUS_CHANGES="true" 557 ;; 558 q) 559 quiet="true" 560 VERBOSE="false" 561 ;; 562 v) 563 VERBOSE="true" 564 quiet="false" 565 ;; 566 *) 567 exit "$FAIL_PREREQ" 568 esac 569 done 570 571 if [ "$quiet" = "true" ]; then 572 exec > /dev/null 2>&1 573 fi 574 575 run "$base_ref" "$past_ref" "$abi_error_log" "$@" 576} 577 578main "$@" 579