Linux kernel mirror (for testing)
git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel
os
linux
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 "$@"