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