xref: /linux/scripts/check-uapi.sh (revision e2683c8868d03382da7e1ce8453b543a043066d1)
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