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