public inbox for libabigail@sourceware.org
 help / color / mirror / Atom feed
* [PATCH v6 0/3] Validating UAPI backwards compatibility
@ 2023-10-27 19:30 John Moon
  2023-10-27 19:30 ` [PATCH v6 1/3] check-uapi: Introduce check-uapi.sh John Moon
                   ` (2 more replies)
  0 siblings, 3 replies; 8+ messages in thread
From: John Moon @ 2023-10-27 19:30 UTC (permalink / raw)
  To: Masahiro Yamada, Nathan Chancellor, Nick Desaulniers,
	Nicolas Schier, Jonathan Corbet
  Cc: John Moon, linux-kbuild, linux-kernel, linux-arm-kernel,
	linux-arm-msm, kernel, Greg Kroah-Hartman, Randy Dunlap,
	Arnd Bergmann, Bjorn Andersson, Todd Kjos, Matthias Maennich,
	Giuliano Procida, kernel-team, libabigail, Dodji Seketeli,
	Trilok Soni, Satya Durga Srinivasu Prabhala, Jordan Crouse

The kernel community has rigorously enforced a policy of backwards
compatibility in its userspace interfaces for a long time. This has
allowed user applications to enjoy stability across kernel upgrades
without recompiling. Our goal is to add tooling and documentation to
help kernel developers maintain this stability.

In terms of tooling, I've attached a couple of shell scripts we've been
internally to validate backwards compatibility of our UAPI headers.

The check-uapi.sh script uses libabigail's[1] tool abidiff[2] to compare a
modified header's ABI before and after a patch is applied. If an existing
UAPI is modified in a way that's not backwards compatibile, the script
exits non-zero. We use this script in our continuous integration system
to block changes that fail the check.

It generates output like this when a backwards incompatible change is
made to a UAPI header:

ABI differences detected in include/uapi/linux/bpf.h from HEAD~1 -> HEAD
    [C] 'struct bpf_insn' changed:
      type size hasn't changed
      2 data member changes:
        '__u8 dst_reg' offset changed from 8 to 12 (in bits) (by +4 bits)
        '__u8 src_reg' offset changed from 12 to 8 (in bits) (by -4 bits)

The check-module-params.sh script is quite a bit simpler. It basically
greps for module_param.* calls and compares their arguments before/after
a change is applied.

We wanted to share these scripts with the community and hopefully also
receive general feedback when it comes to tooling/policy surrounding
UAPI stability.

In the previous version of this patchset, we received feedback that there
were too many false positives flagged by the check-uapi.sh script. To
improve the situation, we've been working with Dodji Seketeli from
the libabigail team to add additional suppressions which filter out
many of the flags which were raised before. To take advantage of these
suppressions, we've raised the minimum abidiff version requirement to
2.4 which was recently released[3].

Big thanks to Dodji and the libabigail team for working on this use case
with us!

Previous discussion on v5 of this patch can be found here[4].

[1] https://sourceware.org/libabigail/manual/libabigail-overview.html
[2] https://sourceware.org/libabigail/manual/abidiff.html
[3] http://mirrors.kernel.org/sourceware/libabigail/libabigail-2.4.tar.xz
[4] https://lore.kernel.org/lkml/20230407203456.27141-1-quic_johmoo@quicinc.com/

P.S. While at Qualcomm, Jordan Crouse <jorcrous@amazon.com> authored the
original version of the UAPI checker script. Thanks Jordan!

John Moon (3):
  check-uapi: Introduce check-uapi.sh
  docs: dev-tools: Add UAPI checker documentation
  check-module-params: Introduce check-module-params.sh

 Documentation/dev-tools/checkuapi.rst | 477 +++++++++++++++++++++
 Documentation/dev-tools/index.rst     |   1 +
 scripts/check-module-params.sh        | 295 +++++++++++++
 scripts/check-uapi.sh                 | 585 ++++++++++++++++++++++++++
 4 files changed, 1358 insertions(+)
 create mode 100644 Documentation/dev-tools/checkuapi.rst
 create mode 100755 scripts/check-module-params.sh
 create mode 100755 scripts/check-uapi.sh


base-commit: fe1998aa935b44ef873193c0772c43bce74f17dc
--
2.17.1


^ permalink raw reply	[flat|nested] 8+ messages in thread

* [PATCH v6 1/3] check-uapi: Introduce check-uapi.sh
  2023-10-27 19:30 [PATCH v6 0/3] Validating UAPI backwards compatibility John Moon
@ 2023-10-27 19:30 ` John Moon
  2023-11-14 10:10   ` Masahiro Yamada
  2023-10-27 19:30 ` [PATCH v6 2/3] docs: dev-tools: Add UAPI checker documentation John Moon
  2023-10-27 19:30 ` [PATCH v6 3/3] check-module-params: Introduce check-module-params.sh John Moon
  2 siblings, 1 reply; 8+ messages in thread
From: John Moon @ 2023-10-27 19:30 UTC (permalink / raw)
  To: Masahiro Yamada, Nathan Chancellor, Nick Desaulniers,
	Nicolas Schier, Jonathan Corbet
  Cc: John Moon, linux-kbuild, linux-kernel, linux-arm-kernel,
	linux-arm-msm, kernel, Greg Kroah-Hartman, Randy Dunlap,
	Arnd Bergmann, Bjorn Andersson, Todd Kjos, Matthias Maennich,
	Giuliano Procida, kernel-team, libabigail, Dodji Seketeli,
	Trilok Soni, Satya Durga Srinivasu Prabhala, Jordan Crouse

While the kernel community has been good at maintaining backwards
compatibility with kernel UAPIs, it would be helpful to have a tool
to check if a commit introduces changes that break backwards
compatibility.

To that end, introduce check-uapi.sh: a simple shell script that
checks for changes to UAPI headers using libabigail.

libabigail is "a framework which aims at helping developers and
software distributors to spot some ABI-related issues like interface
incompatibility in ELF shared libraries by performing a static
analysis of the ELF binaries at hand."

The script uses one of libabigail's tools, "abidiff", to compile the
changed header before and after the commit to detect any changes.

abidiff "compares the ABI of two shared libraries in ELF format. It
emits a meaningful report describing the differences between the two
ABIs."

The script also includes the ability to check the compatibility of
all UAPI headers across commits. This allows developers to inspect
the stability of the UAPIs over time.

Signed-off-by: John Moon <quic_johmoo@quicinc.com>
---
    - Add abidiff suppressions to filter out common things like enum
      variants named .*_MAX being changed and expansion into padding
      fields.
    - Bump minimum abidiff version to 2.4 to accomodate new
      suppressions.
    - Add option (-i) to suppress ambiguous breaking changes.
    - Remove printing of full file diffs when ABI breakage is found
      as this was too noisy.
    - Wait for all files to be checked before printing results as
      printing from parallel threads was garbling output.
    - Suppress all output when -q is passed.
    - Avoid messing up user's git tree by using "git archive" instead
      of checking out references.

 scripts/check-uapi.sh | 585 ++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 585 insertions(+)
 create mode 100755 scripts/check-uapi.sh

diff --git a/scripts/check-uapi.sh b/scripts/check-uapi.sh
new file mode 100755
index 000000000000..bcb03c8dae60
--- /dev/null
+++ b/scripts/check-uapi.sh
@@ -0,0 +1,585 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0-only
+# Script to check commits for UAPI backwards compatibility
+
+set -o errexit
+set -o pipefail
+
+print_usage() {
+	name=$(basename "$0")
+	cat << EOF
+$name - check for UAPI header stability across Git commits
+
+By default, the script will check to make sure the latest commit (or current
+dirty changes) did not introduce ABI changes when compared to HEAD^1. You can
+check against additional commit ranges with the -b and -p options.
+
+The script will not check UAPI headers for architectures other than the one
+defined in ARCH.
+
+Usage: $name [-b BASE_REF] [-p PAST_REF] [-j N] [-l ERROR_LOG] [-i] [-q] [-v]
+
+Options:
+    -b BASE_REF    Base git reference to use for comparison. If unspecified or empty,
+                   will use any dirty changes in tree to UAPI files. If there are no
+                   dirty changes, HEAD will be used.
+    -p PAST_REF    Compare BASE_REF to PAST_REF (e.g. -p v6.1). If unspecified or empty,
+                   will use BASE_REF^1. Must be an ancestor of BASE_REF. Only headers
+                   that exist on PAST_REF will be checked for compatibility.
+    -j JOBS        Number of checks to run in parallel (default: number of CPU cores).
+    -l ERROR_LOG   Write error log to file (default: no error log is generated).
+    -i             Ignore ambiguous changes that may or may not break UAPI compatibility.
+    -q             Quiet operation.
+    -v             Verbose operation (print more information about each header being checked).
+
+Environmental args:
+    ABIDIFF  Custom path to abidiff binary
+    CC       C compiler (default is "gcc")
+    ARCH     Target architecture of C compiler (default is host arch)
+
+Exit codes:
+    $SUCCESS) Success
+    $FAIL_ABI) ABI difference detected
+    $FAIL_PREREQ) Prerequisite not met
+EOF
+}
+
+readonly SUCCESS=0
+readonly FAIL_ABI=1
+readonly FAIL_PREREQ=2
+
+# Print to stderr
+eprintf() {
+	# shellcheck disable=SC2059
+	printf "$@" >&2
+}
+
+# Expand an array with a specific character (similar to Python string.join())
+join() {
+	local IFS="$1"
+	shift
+	printf "%s" "$*"
+}
+
+# Create abidiff suppressions
+gen_suppressions() {
+	# Common enum variant names which we don't want to worry about
+	# being shifted when new variants are added.
+	local -a enum_regex=(
+		".*_AFTER_LAST$"
+		".*_CNT$"
+		".*_COUNT$"
+		".*_END$"
+		".*_LAST$"
+		".*_MASK$"
+		".*_MAX$"
+		".*_MAX_BIT$"
+		".*_MAX_BPF_ATTACH_TYPE$"
+		".*_MAX_ID$"
+		".*_MAX_SHIFT$"
+		".*_NBITS$"
+		".*_NETDEV_NUMHOOKS$"
+		".*_NFT_META_IIFTYPE$"
+		".*_NL80211_ATTR$"
+		".*_NLDEV_NUM_OPS$"
+		".*_NUM$"
+		".*_NUM_ELEMS$"
+		".*_NUM_IRQS$"
+		".*_SIZE$"
+		".*_TLSMAX$"
+		"^MAX_.*"
+		"^NUM_.*"
+	)
+
+	# Common padding field names which can be expanded into
+	# without worrying about users.
+	local -a padding_regex=(
+		".*end$"
+		".*pad$"
+		".*pad[0-9]?$"
+		".*pad_[0-9]?$"
+		".*padding$"
+		".*padding[0-9]?$"
+		".*padding_[0-9]?$"
+		".*res$"
+		".*resv$"
+		".*resv[0-9]?$"
+		".*resv_[0-9]?$"
+		".*reserved$"
+		".*reserved[0-9]?$"
+		".*reserved_[0-9]?$"
+		".*rsvd[0-9]?$"
+		".*unused$"
+	)
+
+	cat << EOF
+[suppress_type]
+  type_kind = enum
+  changed_enumerators_regexp = $(join , "${enum_regex[@]}")
+EOF
+
+	for p in "${padding_regex[@]}"; do
+		cat << EOF
+[suppress_type]
+  type_kind = struct
+  has_data_member_inserted_at = offset_of_first_data_member_regexp(${p})
+EOF
+	done
+
+if [ "$IGNORE_AMBIGUOUS_CHANGES" = "true" ]; then
+	cat << EOF
+[suppress_type]
+  type_kind = struct
+  has_data_member_inserted_at = end
+  has_size_change = yes
+EOF
+fi
+}
+
+# Check if git tree is dirty
+tree_is_dirty() {
+	! git diff --quiet
+}
+
+# Get list of files installed in $ref
+get_file_list() {
+	local -r ref="$1"
+	local -r tree="$(get_header_tree "$ref")"
+
+	# Print all installed headers, filtering out ones that can't be compiled
+	find "$tree" -type f -name '*.h' -printf '%P\n' | grep -v -f "$INCOMPAT_LIST"
+}
+
+# Add to the list of incompatible headers
+add_to_incompat_list() {
+	local -r ref="$1"
+
+	# Start with the usr/include/Makefile to get a list of the headers
+	# that don't compile using this method.
+	if [ ! -f usr/include/Makefile ]; then
+		eprintf "error - no usr/include/Makefile present at %s\n" "$ref"
+		eprintf "Note: usr/include/Makefile was added in the v5.3 kernel release\n"
+		exit "$FAIL_PREREQ"
+	fi
+	{
+		# shellcheck disable=SC2016
+		printf 'all: ; @echo $(no-header-test)\n'
+		cat usr/include/Makefile
+	} | SRCARCH="$ARCH" make --always-make -f - | tr " " "\n" \
+	  | grep -v "asm-generic" >> "$INCOMPAT_LIST"
+
+	# The makefile also skips all asm-generic files, but prints "asm-generic/%"
+	# which won't work for our grep match. Instead, print something grep will match.
+	printf "asm-generic/.*\.h\n" >> "$INCOMPAT_LIST"
+
+	sort -u -o "$INCOMPAT_LIST" "$INCOMPAT_LIST"
+	sed -i -e '/^$/d' "$INCOMPAT_LIST"
+}
+
+# Compile the simple test app
+do_compile() {
+	local -r inc_dir="$1"
+	local -r header="$2"
+	local -r out="$3"
+	printf "int main(void) { return 0; }\n" | \
+		"$CC" -c \
+		  -o "$out" \
+		  -x c \
+		  -O0 \
+		  -std=c90 \
+		  -fno-eliminate-unused-debug-types \
+		  -g \
+		  "-I${inc_dir}" \
+		  -include "$header" \
+		  -
+}
+
+# Run make headers_install
+run_make_headers_install() {
+	local -r install_dir="$1"
+	make -j "$MAX_THREADS" ARCH="$ARCH" INSTALL_HDR_PATH="$install_dir" \
+		headers_install > /dev/null
+}
+
+# Install headers for both git refs
+install_headers() {
+	local -r base_ref="$1"
+	local -r past_ref="$2"
+
+	for ref in "$base_ref" "$past_ref"; do
+		printf "Installing user-facing UAPI headers from %s... " "${ref:-dirty tree}"
+		if [ -n "$ref" ]; then
+			git archive --format=tar --prefix="${ref}-archive/" "$ref" \
+				| (cd "$TMP_DIR" && tar xf -)
+			(
+				cd "${TMP_DIR}/${ref}-archive"
+				run_make_headers_install "${TMP_DIR}/${ref}/usr"
+				add_to_incompat_list "$ref" "$INCOMPAT_LIST"
+			)
+		else
+			run_make_headers_install "${TMP_DIR}/${ref}/usr"
+			add_to_incompat_list "$ref" "$INCOMPAT_LIST"
+		fi
+		printf "OK\n"
+	done
+}
+
+# Print the path to the headers_install tree for a given ref
+get_header_tree() {
+	local -r ref="$1"
+	printf "%s" "${TMP_DIR}/${ref}/usr"
+}
+
+# Check file list for UAPI compatibility
+check_uapi_files() {
+	local -r base_ref="$1"
+	local -r past_ref="$2"
+	local -r abi_error_log="$3"
+
+	local passed=0;
+	local failed=0;
+	local -a threads=()
+	set -o errexit
+
+	printf "Checking changes to UAPI headers between %s and %s...\n" "$past_ref" "${base_ref:-dirty tree}"
+	# Loop over all UAPI headers that were installed by $past_ref (if they only exist on $base_ref,
+	# there's no way they're broken and no way to compare anyway)
+	while read -r file; do
+		if [ "${#threads[@]}" -ge "$MAX_THREADS" ]; then
+			if wait "${threads[0]}"; then
+				passed=$((passed + 1))
+			else
+				failed=$((failed + 1))
+			fi
+			threads=("${threads[@]:1}")
+		fi
+
+		check_individual_file "$base_ref" "$past_ref" "$file" &
+		threads+=("$!")
+	done < <(get_file_list "$past_ref")
+
+	for t in "${threads[@]}"; do
+		if wait "$t"; then
+			passed=$((passed + 1))
+		else
+			failed=$((failed + 1))
+		fi
+	done
+
+	if [ -n "$abi_error_log" ]; then
+		printf 'Generated by "%s %s" from git ref %s\n\n' \
+			"$0" "$*" "$(git rev-parse HEAD)" > "$abi_error_log"
+	fi
+
+	while read -r error_file; do
+		{
+			cat "$error_file"
+			printf "\n\n"
+		} | tee -a "${abi_error_log:-/dev/null}" >&2
+	done < <(find "$TMP_DIR" -type f -name '*.error')
+
+	total="$((passed + failed))"
+	if [ "$failed" -gt 0 ]; then
+		eprintf "error - %d/%d UAPI headers compatible with %s appear _not_ to be backwards compatible\n" \
+			"$failed" "$total" "$ARCH"
+		if [ -n "$abi_error_log" ]; then
+			eprintf "Failure summary saved to %s\n" "$abi_error_log"
+		fi
+	else
+		printf "All %d UAPI headers compatible with %s appear to be backwards compatible\n" \
+			"$total" "$ARCH"
+	fi
+
+	return "$failed"
+}
+
+# Check an individual file for UAPI compatibility
+check_individual_file() {
+	local -r base_ref="$1"
+	local -r past_ref="$2"
+	local -r file="$3"
+
+	local -r base_header="$(get_header_tree "$base_ref")/${file}"
+	local -r past_header="$(get_header_tree "$past_ref")/${file}"
+
+	local base_ref_short="${base_ref:-dirty tree}"
+	if [ "${#base_ref_short}" -ge 40 ]; then
+		base_ref_short="$(git rev-parse --short "$base_ref_short")"
+	fi
+
+	local past_ref_short="$past_ref"
+	if [ "${#past_ref_short}" -ge 40 ]; then
+		past_ref_short="$(git rev-parse --short "$past_ref_short")"
+	fi
+
+	if [ ! -f "$base_header" ]; then
+		mkdir -p "$(dirname "$base_header")"
+		printf "==== UAPI header %s was removed between %s and %s ====" \
+			"$file" "$past_ref_short" "$base_ref_short" \
+				> "${base_header}.error"
+		return 1
+	fi
+
+	compare_abi "$file" "$base_header" "$past_header" "$base_ref" "$past_ref" \
+		"$base_ref_short" "$past_ref_short"
+}
+
+# Perform the A/B compilation and compare output ABI
+compare_abi() {
+	local -r file="$1"
+	local -r base_header="$2"
+	local -r past_header="$3"
+	local -r base_ref="$4"
+	local -r past_ref="$5"
+	local -r base_ref_short="$6"
+	local -r past_ref_short="$7"
+	local -r log="${TMP_DIR}/log/${file}.log"
+
+	mkdir -p "$(dirname "$log")"
+
+	if ! do_compile "$(get_header_tree "$base_ref")/include" "$base_header" "${base_header}.bin" 2> "$log"; then
+		{
+			warn_str=$(printf "==== Could not compile version of UAPI header %s at %s ====\n" \
+				"$file" "$base_ref_short")
+			printf "%s\n" "$warn_str"
+			cat "$log"
+			printf -- "=%.0s" $(seq 0 ${#warn_str})
+		} > "${base_header}.error"
+		return 1
+	fi
+
+	if ! do_compile "$(get_header_tree "$past_ref")/include" "$past_header" "${past_header}.bin" 2> "$log"; then
+		{
+			warn_str=$(printf "==== Could not compile version of UAPI header %s at %s ====\n" \
+				"$file" "$past_ref_short")
+			printf "%s\n" "$warn_str"
+			cat "$log"
+			printf -- "=%.0s" $(seq 0 ${#warn_str})
+		} > "${base_header}.error"
+		return 1
+	fi
+
+	local ret=0
+	"$ABIDIFF" --non-reachable-types \
+		--suppressions "$SUPPRESSIONS" \
+		"${past_header}.bin" "${base_header}.bin" > "$log" || ret="$?"
+	if [ "$ret" -eq 0 ]; then
+		if [ "$VERBOSE" = "true" ]; then
+			printf "No ABI differences detected in %s from %s -> %s\n" \
+				"$file" "$past_ref_short" "$base_ref_short"
+		fi
+	else
+		# Bits in abidiff's return code can be used to determine the type of error
+		if [ $((ret & 0x2)) -gt 0 ]; then
+			eprintf "error - abidiff did not run properly\n"
+			exit 1
+		fi
+
+		if [ "$IGNORE_AMBIGUOUS_CHANGES" = "true" ] && [ "$ret" -eq 4 ]; then
+			return 0
+		fi
+
+		# If the only changes were additions (not modifications to existing APIs), then
+		# there's no problem. Ignore these diffs.
+		if grep "Unreachable types summary" "$log" | grep -q "0 removed" &&
+		   grep "Unreachable types summary" "$log" | grep -q "0 changed"; then
+			return 0
+		fi
+
+		{
+			warn_str=$(printf "==== ABI differences detected in %s from %s -> %s ====" \
+				"$file" "$past_ref_short" "$base_ref_short")
+			printf "%s\n" "$warn_str"
+			sed  -e '/summary:/d' -e '/changed type/d' -e '/^$/d' -e 's/^/  /g' "$log"
+			printf -- "=%.0s" $(seq 0 ${#warn_str})
+			if cmp "$past_header" "$base_header" > /dev/null 2>&1; then
+				printf "\n%s did not change between %s and %s...\n" "$file" "$past_ref" "${base_ref:-dirty tree}"
+				printf "It's possible a change to one of the headers it includes caused this error:\n"
+				grep '^#include' "$base_header"
+				printf "\n"
+			fi
+		} > "${base_header}.error"
+
+		return 1
+	fi
+}
+
+# Check that a minimum software version number is satisfied
+min_version_is_satisfied() {
+	local -r min_version="$1"
+	local -r version_installed="$2"
+
+	printf "%s\n%s\n" "$min_version" "$version_installed" \
+		| sort -Vc > /dev/null 2>&1
+}
+
+# Make sure we have the tools we need and the arguments make sense
+check_deps() {
+	ABIDIFF="${ABIDIFF:-abidiff}"
+	CC="${CC:-gcc}"
+	ARCH="${ARCH:-$(uname -m)}"
+	if [ "$ARCH" = "x86_64" ]; then
+		ARCH="x86"
+	fi
+
+	local -r abidiff_min_version="2.4"
+	local -r libdw_min_version_if_clang="0.171"
+
+	if ! command -v "$ABIDIFF" > /dev/null 2>&1; then
+		eprintf "error - abidiff not found!\n"
+		eprintf "Please install abigail-tools version %s or greater\n" "$abidiff_min_version"
+		eprintf "See: https://sourceware.org/libabigail/manual/libabigail-overview.html\n"
+		return 1
+	fi
+
+	local -r abidiff_version="$("$ABIDIFF" --version | cut -d ' ' -f 2)"
+	if ! min_version_is_satisfied "$abidiff_min_version" "$abidiff_version"; then
+		eprintf "error - abidiff version too old: %s\n" "$abidiff_version"
+		eprintf "Please install abigail-tools version %s or greater\n" "$abidiff_min_version"
+		eprintf "See: https://sourceware.org/libabigail/manual/libabigail-overview.html\n"
+		return 1
+	fi
+
+	if ! command -v "$CC" > /dev/null 2>&1; then
+		eprintf 'error - %s not found\n' "$CC"
+		return 1
+	fi
+
+	if "$CC" --version | grep -q clang; then
+		local -r libdw_version="$(ldconfig -v 2>/dev/null | grep -v SKIPPED | grep -m 1 -o 'libdw-[0-9]\+.[0-9]\+' | cut -c 7-)"
+		if ! min_version_is_satisfied "$libdw_min_version_if_clang" "$libdw_version"; then
+			eprintf "error - libdw version too old for use with clang: %s\n" "$libdw_version"
+			eprintf "Please install libdw from elfutils version %s or greater\n" "$libdw_min_version_if_clang"
+			eprintf "See: https://sourceware.org/elfutils/\n"
+			return 1
+		fi
+	fi
+
+	if [ ! -d "arch/${ARCH}" ]; then
+		eprintf 'error - ARCH "%s" is not a subdirectory under arch/\n' "$ARCH"
+		eprintf "Please set ARCH to one of:\n%s\n" "$(find arch -maxdepth 1 -mindepth 1 -type d -printf '%f ' | fmt)"
+		return 1
+	fi
+
+	if ! git rev-parse --is-inside-work-tree > /dev/null 2>&1; then
+		eprintf "error - this script requires the kernel tree to be initialized with Git\n"
+		return 1
+	fi
+
+	if ! git rev-parse --verify "$past_ref" > /dev/null 2>&1; then
+		printf 'error - invalid git reference "%s"\n' "$past_ref"
+		return 1
+	fi
+
+	if [ -n "$base_ref" ]; then
+		if ! git merge-base --is-ancestor "$past_ref" "$base_ref" > /dev/null 2>&1; then
+			printf 'error - "%s" is not an ancestor of base ref "%s"\n' "$past_ref" "$base_ref"
+			return 1
+		fi
+		if [ "$(git rev-parse "$base_ref")" = "$(git rev-parse "$past_ref")" ]; then
+			printf 'error - "%s" and "%s" are the same reference\n' "$past_ref" "$base_ref"
+			return 1
+		fi
+	fi
+}
+
+run() {
+	local base_ref="$1"
+	local past_ref="$2"
+	local abi_error_log="$3"
+	shift 3
+
+	if [ -z "$KERNEL_SRC" ]; then
+		KERNEL_SRC="$(realpath "$(dirname "$0")"/..)"
+	fi
+
+	cd "$KERNEL_SRC"
+
+	if [ -z "$base_ref" ] && ! tree_is_dirty; then
+		base_ref=HEAD
+	fi
+
+	if [ -z "$past_ref" ]; then
+		if [ -n "$base_ref" ]; then
+			past_ref="${base_ref}^1"
+		else
+			past_ref=HEAD
+		fi
+	fi
+
+	if ! check_deps; then
+		exit "$FAIL_PREREQ"
+	fi
+
+	TMP_DIR=$(mktemp -d)
+	readonly TMP_DIR
+	trap 'rm -rf "$TMP_DIR"' EXIT
+
+	readonly INCOMPAT_LIST="${TMP_DIR}/incompat_list.txt"
+	touch "$INCOMPAT_LIST"
+
+	readonly SUPPRESSIONS="${TMP_DIR}/suppressions.txt"
+	gen_suppressions > "$SUPPRESSIONS"
+
+	# Run make install_headers for both refs
+	install_headers "$base_ref" "$past_ref"
+
+	# Check for any differences in the installed header trees
+	if diff -r -q "$(get_header_tree "$base_ref")" "$(get_header_tree "$past_ref")" > /dev/null 2>&1; then
+		printf "No changes to UAPI headers were applied between %s and %s\n" "$past_ref" "${base_ref:-dirty tree}"
+		exit "$SUCCESS"
+	fi
+
+	if ! check_uapi_files "$base_ref" "$past_ref" "$abi_error_log"; then
+		exit "$FAIL_ABI"
+	fi
+}
+
+main() {
+	MAX_THREADS=$(nproc)
+	VERBOSE="false"
+	IGNORE_AMBIGUOUS_CHANGES="false"
+	quiet="false"
+	local base_ref=""
+	while getopts "hb:p:j:l:iqv" opt; do
+		case $opt in
+		h)
+			print_usage
+			exit "$SUCCESS"
+			;;
+		b)
+			base_ref="$OPTARG"
+			;;
+		p)
+			past_ref="$OPTARG"
+			;;
+		j)
+			MAX_THREADS="$OPTARG"
+			;;
+		l)
+			abi_error_log="$OPTARG"
+			;;
+		i)
+			IGNORE_AMBIGUOUS_CHANGES="true"
+			;;
+		q)
+			quiet="true"
+			VERBOSE="false"
+			;;
+		v)
+			VERBOSE="true"
+			quiet="false"
+			;;
+		*)
+			exit "$FAIL_PREREQ"
+		esac
+	done
+
+	if [ "$quiet" = "true" ]; then
+		exec > /dev/null 2>&1
+	fi
+
+	run "$base_ref" "$past_ref" "$abi_error_log" "$@"
+}
+
+main "$@"
--
2.17.1


^ permalink raw reply	[flat|nested] 8+ messages in thread

* [PATCH v6 2/3] docs: dev-tools: Add UAPI checker documentation
  2023-10-27 19:30 [PATCH v6 0/3] Validating UAPI backwards compatibility John Moon
  2023-10-27 19:30 ` [PATCH v6 1/3] check-uapi: Introduce check-uapi.sh John Moon
@ 2023-10-27 19:30 ` John Moon
  2023-10-27 19:30 ` [PATCH v6 3/3] check-module-params: Introduce check-module-params.sh John Moon
  2 siblings, 0 replies; 8+ messages in thread
From: John Moon @ 2023-10-27 19:30 UTC (permalink / raw)
  To: Masahiro Yamada, Nathan Chancellor, Nick Desaulniers,
	Nicolas Schier, Jonathan Corbet
  Cc: John Moon, linux-kbuild, linux-kernel, linux-arm-kernel,
	linux-arm-msm, kernel, Greg Kroah-Hartman, Randy Dunlap,
	Arnd Bergmann, Bjorn Andersson, Todd Kjos, Matthias Maennich,
	Giuliano Procida, kernel-team, libabigail, Dodji Seketeli,
	Trilok Soni, Satya Durga Srinivasu Prabhala, Jordan Crouse

Add detailed documentation for scripts/check-uapi.sh.

Signed-off-by: John Moon <quic_johmoo@quicinc.com>
---
    - Updated with new tool output.
    - Reworked section on "false positives" and instead list caveats
      of using the tool.

 Documentation/dev-tools/checkuapi.rst | 477 ++++++++++++++++++++++++++
 Documentation/dev-tools/index.rst     |   1 +
 2 files changed, 478 insertions(+)
 create mode 100644 Documentation/dev-tools/checkuapi.rst

diff --git a/Documentation/dev-tools/checkuapi.rst b/Documentation/dev-tools/checkuapi.rst
new file mode 100644
index 000000000000..9072f21b50b0
--- /dev/null
+++ b/Documentation/dev-tools/checkuapi.rst
@@ -0,0 +1,477 @@
+.. SPDX-License-Identifier: GPL-2.0-only
+
+============
+UAPI Checker
+============
+
+The UAPI checker (``scripts/check-uapi.sh``) is a shell script which
+checks UAPI header files for userspace backwards-compatibility across
+the git tree.
+
+Options
+=======
+
+This section will describe the options with which ``check-uapi.sh``
+can be run.
+
+Usage::
+
+    check-uapi.sh [-b BASE_REF] [-p PAST_REF] [-j N] [-l ERROR_LOG] [-i] [-q] [-v]
+
+Available options::
+
+    -b BASE_REF    Base git reference to use for comparison. If unspecified or empty,
+                   will use any dirty changes in tree to UAPI files. If there are no
+                   dirty changes, HEAD will be used.
+    -p PAST_REF    Compare BASE_REF to PAST_REF (e.g. -p v6.1). If unspecified or empty,
+                   will use BASE_REF^1. Must be an ancestor of BASE_REF. Only headers
+                   that exist on PAST_REF will be checked for compatibility.
+    -j JOBS        Number of checks to run in parallel (default: number of CPU cores).
+    -l ERROR_LOG   Write error log to file (default: no error log is generated).
+    -i             Ignore ambiguous changes that may or may not break UAPI compatibility.
+    -q             Quiet operation.
+    -v             Verbose operation (print more information about each header being checked).
+
+Environmental args::
+
+    ABIDIFF  Custom path to abidiff binary
+    CC       C compiler (default is "gcc")
+    ARCH     Target architecture of C compiler (default is host arch)
+
+Exit codes::
+
+    0) Success
+    1) ABI difference detected
+    2) Prerequisite not met
+
+Examples
+========
+
+Basic Usage
+-----------
+
+First, let's try making a change to a UAPI header file that obviously
+won't break userspace::
+
+    cat << 'EOF' | patch -l -p1
+    --- a/include/uapi/linux/acct.h
+    +++ b/include/uapi/linux/acct.h
+    @@ -21,7 +21,9 @@
+     #include <asm/param.h>
+     #include <asm/byteorder.h>
+
+    -/*
+    +#define FOO
+    +
+    +/*
+      *  comp_t is a 16-bit "floating" point number with a 3-bit base 8
+      *  exponent and a 13-bit fraction.
+      *  comp2_t is 24-bit with 5-bit base 2 exponent and 20 bit fraction
+    diff --git a/include/uapi/linux/bpf.h b/include/uapi/linux/bpf.h
+    EOF
+
+Now, let's use the script to validate::
+
+    % ./scripts/check-uapi.sh
+    Installing user-facing UAPI headers from dirty tree... OK
+    Installing user-facing UAPI headers from HEAD... OK
+    Checking changes to UAPI headers between HEAD and dirty tree...
+    All 912 UAPI headers compatible with x86 appear to be backwards compatible
+
+Let's add another change that *might* break userspace::
+
+    cat << 'EOF' | patch -l -p1
+    --- a/include/uapi/linux/bpf.h
+    +++ b/include/uapi/linux/bpf.h
+    @@ -74,7 +74,7 @@ struct bpf_insn {
+            __u8    dst_reg:4;      /* dest register */
+            __u8    src_reg:4;      /* source register */
+            __s16   off;            /* signed offset */
+    -       __s32   imm;            /* signed immediate constant */
+    +       __u32   imm;            /* unsigned immediate constant */
+     };
+
+     /* Key of an a BPF_MAP_TYPE_LPM_TRIE entry */
+    EOF
+
+The script will catch this::
+
+    % ./scripts/check-uapi.sh
+    Installing user-facing UAPI headers from dirty tree... OK
+    Installing user-facing UAPI headers from HEAD... OK
+    Checking changes to UAPI headers between HEAD and dirty tree...
+    ==== ABI differences detected in include/linux/bpf.h from HEAD -> dirty tree ====
+        [C] 'struct bpf_insn' changed:
+          type size hasn't changed
+          1 data member change:
+            type of '__s32 imm' changed:
+              typedef name changed from __s32 to __u32 at int-ll64.h:27:1
+              underlying type 'int' changed:
+                type name changed from 'int' to 'unsigned int'
+                type size hasn't changed
+    ==================================================================================
+
+    error - 1/912 UAPI headers compatible with x86 appear _not_ to be backwards compatible
+
+In this case, the script is reporting the type change because it could
+break a userspace program that passes in a negative number. Now, let's
+say you know that no userspace program could possibly be using a negative
+value in ``imm``, so changing to an unsigned type there shouldn't hurt
+anything. You can pass the ``-i`` flag to the script to ignore changes
+in which the userspace backwards compatibility is ambiguous::
+
+    % ./scripts/check-uapi.sh -i
+    Installing user-facing UAPI headers from dirty tree... OK
+    Installing user-facing UAPI headers from HEAD... OK
+    Checking changes to UAPI headers between HEAD and dirty tree...
+    All 912 UAPI headers compatible with x86 appear to be backwards compatible
+
+Now, let's make a similar change that *will* break userspace::
+
+    cat << 'EOF' | patch -l -p1
+    --- a/include/uapi/linux/bpf.h
+    +++ b/include/uapi/linux/bpf.h
+    @@ -71,8 +71,8 @@ enum {
+
+     struct bpf_insn {
+            __u8    code;           /* opcode */
+    -       __u8    dst_reg:4;      /* dest register */
+            __u8    src_reg:4;      /* source register */
+    +       __u8    dst_reg:4;      /* dest register */
+            __s16   off;            /* signed offset */
+            __s32   imm;            /* signed immediate constant */
+     };
+    EOF
+
+Since we're re-ordering an existing struct member, there's no ambiguity,
+and the script will report the breakage even if you pass ``-i``::
+
+    % ./scripts/check-uapi.sh -i
+    Installing user-facing UAPI headers from dirty tree... OK
+    Installing user-facing UAPI headers from HEAD... OK
+    Checking changes to UAPI headers between HEAD and dirty tree...
+    ==== ABI differences detected in include/linux/bpf.h from HEAD -> dirty tree ====
+        [C] 'struct bpf_insn' changed:
+          type size hasn't changed
+          2 data member changes:
+            '__u8 dst_reg' offset changed from 8 to 12 (in bits) (by +4 bits)
+            '__u8 src_reg' offset changed from 12 to 8 (in bits) (by -4 bits)
+    ==================================================================================
+
+    error - 1/912 UAPI headers compatible with x86 appear _not_ to be backwards compatible
+
+Let's commit the breaking change, then commit the innocuous change::
+
+    % git commit -m 'Breaking UAPI change' include/uapi/linux/bpf.h
+    [detached HEAD f758e574663a] Breaking UAPI change
+     1 file changed, 1 insertion(+), 1 deletion(-)
+    % git commit -m 'Innocuous UAPI change' include/uapi/linux/acct.h
+    [detached HEAD 2e87df769081] Innocuous UAPI change
+     1 file changed, 3 insertions(+), 1 deletion(-)
+
+Now, let's run the script again with no arguments::
+
+    % ./scripts/check-uapi.sh
+    Installing user-facing UAPI headers from HEAD... OK
+    Installing user-facing UAPI headers from HEAD^1... OK
+    Checking changes to UAPI headers between HEAD^1 and HEAD...
+    All 912 UAPI headers compatible with x86 appear to be backwards compatible
+
+It doesn't catch any breaking change because, by default, it only
+compares ``HEAD`` to ``HEAD^1``. The breaking change was committed on
+``HEAD~2``. If we wanted the search scope to go back further, we'd have to
+use the ``-p`` option to pass a different past reference. In this case,
+let's pass ``-p HEAD~2`` to the script so it checks UAPI changes between
+``HEAD~2`` and ``HEAD``::
+
+    % ./scripts/check-uapi.sh -p HEAD~2
+    Installing user-facing UAPI headers from HEAD... OK
+    Installing user-facing UAPI headers from HEAD~2... OK
+    Checking changes to UAPI headers between HEAD~2 and HEAD...
+    ==== ABI differences detected in include/linux/bpf.h from HEAD~2 -> HEAD ====
+        [C] 'struct bpf_insn' changed:
+          type size hasn't changed
+          2 data member changes:
+            '__u8 dst_reg' offset changed from 8 to 12 (in bits) (by +4 bits)
+            '__u8 src_reg' offset changed from 12 to 8 (in bits) (by -4 bits)
+    ==============================================================================
+
+    error - 1/912 UAPI headers compatible with x86 appear _not_ to be backwards compatible
+
+Alternatively, we could have also run with ``-b HEAD~``. This would set the
+base reference to ``HEAD~`` so then the script would compare it to ``HEAD~^1``.
+
+Architecture-specific Headers
+-----------------------------
+
+Consider this change::
+
+    cat << 'EOF' | patch -l -p1
+    --- a/arch/arm64/include/uapi/asm/sigcontext.h
+    +++ b/arch/arm64/include/uapi/asm/sigcontext.h
+    @@ -70,6 +70,7 @@ struct sigcontext {
+     struct _aarch64_ctx {
+            __u32 magic;
+            __u32 size;
+    +       __u32 new_var;
+     };
+
+     #define FPSIMD_MAGIC   0x46508001
+    EOF
+
+This is a change to an arm64-specific UAPI header file. In this example, I'm
+running the script from an x86 machine with an x86 compiler, so, by default,
+the script only checks x86-compatible UAPI header files::
+
+    % ./scripts/check-uapi.sh
+    Installing user-facing UAPI headers from dirty tree... OK
+    Installing user-facing UAPI headers from HEAD... OK
+    No changes to UAPI headers were applied between HEAD and dirty tree
+
+With an x86 compiler, we can't check header files in ``arch/arm64``, so the
+script doesn't even try.
+
+If we want to check the header file, we'll have to use an arm64 compiler and
+set ``ARCH`` accordingly::
+
+    % CC=aarch64-linux-gnu-gcc ARCH=arm64 ./scripts/check-uapi.sh
+    Installing user-facing UAPI headers from dirty tree... OK
+    Installing user-facing UAPI headers from HEAD... OK
+    Checking changes to UAPI headers between HEAD and dirty tree...
+    ==== ABI differences detected in include/asm/sigcontext.h from HEAD -> dirty tree ====
+        [C] 'struct _aarch64_ctx' changed:
+          type size changed from 64 to 96 (in bits)
+          1 data member insertion:
+            '__u32 new_var', at offset 64 (in bits) at sigcontext.h:73:1
+        -- snip --
+        [C] 'struct zt_context' changed:
+          type size changed from 128 to 160 (in bits)
+          2 data member changes (1 filtered):
+            '__u16 nregs' offset changed from 64 to 96 (in bits) (by +32 bits)
+            '__u16 __reserved[3]' offset changed from 80 to 112 (in bits) (by +32 bits)
+    =======================================================================================
+
+    error - 1/884 UAPI headers compatible with arm64 appear _not_ to be backwards compatible
+
+We can see with ``ARCH`` and ``CC`` set properly for the file, the ABI
+change is reported properly. Also notice that the total number of UAPI
+header files checked by the script changes. This is because the number
+of headers installed for arm64 platforms is different than x86.
+
+Cross-Dependency Breakages
+--------------------------
+
+Consider this change::
+
+    cat << 'EOF' | patch -l -p1
+    --- a/include/uapi/linux/types.h
+    +++ b/include/uapi/linux/types.h
+    @@ -52,7 +52,7 @@ typedef __u32 __bitwise __wsum;
+     #define __aligned_be64 __be64 __attribute__((aligned(8)))
+     #define __aligned_le64 __le64 __attribute__((aligned(8)))
+
+    -typedef unsigned __bitwise __poll_t;
+    +typedef unsigned short __bitwise __poll_t;
+
+     #endif /*  __ASSEMBLY__ */
+     #endif /* _UAPI_LINUX_TYPES_H */
+    EOF
+
+Here, we're changing a ``typedef`` in ``types.h``. This doesn't break
+a UAPI in ``types.h``, but other UAPIs in the tree may break due to
+this change::
+
+    % ./scripts/check-uapi.sh
+    Installing user-facing UAPI headers from dirty tree... OK
+    Installing user-facing UAPI headers from HEAD... OK
+    Checking changes to UAPI headers between HEAD and dirty tree...
+    ==== ABI differences detected in include/linux/eventpoll.h from HEAD -> dirty tree ====
+        [C] 'struct epoll_event' changed:
+          type size changed from 96 to 80 (in bits)
+          2 data member changes:
+            type of '__poll_t events' changed:
+              underlying type 'unsigned int' changed:
+                type name changed from 'unsigned int' to 'unsigned short int'
+                type size changed from 32 to 16 (in bits)
+            '__u64 data' offset changed from 32 to 16 (in bits) (by -16 bits)
+    ========================================================================================
+    include/linux/eventpoll.h did not change between HEAD and dirty tree...
+    It's possible a change to one of the headers it includes caused this error:
+    #include <linux/fcntl.h>
+    #include <linux/types.h>
+
+Note that the script noticed the failing header file did not change,
+so it assumes one of its includes must have caused the breakage. Indeed,
+we can see ``linux/types.h`` is used from ``eventpoll.h``.
+
+UAPI Header Removals
+--------------------
+
+Consider this change::
+
+    cat << 'EOF' | patch -l -p1
+    diff --git a/include/uapi/asm-generic/Kbuild b/include/uapi/asm-generic/Kbuild
+    index ebb180aac74e..a9c88b0a8b3b 100644
+    --- a/include/uapi/asm-generic/Kbuild
+    +++ b/include/uapi/asm-generic/Kbuild
+    @@ -31,6 +31,6 @@ mandatory-y += stat.h
+     mandatory-y += statfs.h
+     mandatory-y += swab.h
+     mandatory-y += termbits.h
+    -mandatory-y += termios.h
+    +#mandatory-y += termios.h
+     mandatory-y += types.h
+     mandatory-y += unistd.h
+    EOF
+
+This script removes a UAPI header file from the install list. Let's run
+the script::
+
+    % ./scripts/check-uapi.sh
+    Installing user-facing UAPI headers from dirty tree... OK
+    Installing user-facing UAPI headers from HEAD... OK
+    Checking changes to UAPI headers between HEAD and dirty tree...
+    ==== UAPI header include/asm/termios.h was removed between HEAD and dirty tree ====
+
+    error - 1/912 UAPI headers compatible with x86 appear _not_ to be backwards compatible
+
+Removing a UAPI header is considered a breaking change, and the script
+will flag it as such.
+
+Checking Historic UAPI Compatibility
+------------------------------------
+
+You can use the ``-b`` and ``-p`` options to examine different chunks of your
+git tree. For example, to check all changed UAPI header files between tags
+v6.0 and v6.1, you'd run::
+
+    % ./scripts/check-uapi.sh -b v6.1 -p v6.0
+    Installing user-facing UAPI headers from v6.1... OK
+    Installing user-facing UAPI headers from v6.0... OK
+    Checking changes to UAPI headers between v6.0 and v6.1...
+
+    --- snip ---
+    error - 37/907 UAPI headers compatible with x86 appear _not_ to be backwards compatible
+
+Note: Before v5.3, a header file needed by the script is not present,
+so the script is unable to check changes before then.
+
+You'll notice that the script detected many UAPI changes that are not
+backwards compatible. Knowing that kernel UAPIs are supposed to be stable
+forever, this is an alarming result. This brings us to the next section:
+caveats.
+
+Caveats
+=======
+
+The UAPI checker makes no assumptions about the author's intention, so some
+types of changes may be flagged even though they intentionally break UAPI.
+
+Removals For Refactoring or Deprecation
+---------------------------------------
+
+Sometimes drivers for very old hardware are removed, such as in this example::
+
+    % ./scripts/check-uapi.sh -b ba47652ba655
+    Installing user-facing UAPI headers from ba47652ba655... OK
+    Installing user-facing UAPI headers from ba47652ba655^1... OK
+    Checking changes to UAPI headers between ba47652ba655^1 and ba47652ba655...
+    ==== UAPI header include/linux/meye.h was removed between ba47652ba655^1 and ba47652ba655 ====
+
+    error - 1/910 UAPI headers compatible with x86 appear _not_ to be backwards compatible
+
+The script will always flag removals (even if they're intentional).
+
+Struct Expansions
+-----------------
+
+Depending on how a structure is handled in kernelspace, a change which
+expands a struct could be non-breaking.
+
+If a struct is used as the argument to an ioctl, then the kernel driver
+must be able to handle ioctl commands of any size. Beyond that, you need
+to be careful when copying data from the user. Say, for example, that
+``struct foo`` is changed like this::
+
+    struct foo {
+        __u64 a; /* added in version 1 */
+    +   __u32 b; /* added in version 2 */
+    +   __u32 c; /* added in version 2 */
+    }
+
+By default, the script will flag this kind of change for further review::
+
+    [C] 'struct foo' changed:
+      type size changed from 64 to 128 (in bits)
+      2 data member insertions:
+        '__u32 b', at offset 64 (in bits)
+        '__u32 c', at offset 96 (in bits)
+
+However, it is possible that this change was made safely.
+
+If a userspace program was built with version 1, it will think
+``sizeof(struct foo)`` is 8. That size will be encoded in the
+ioctl value that gets sent to the kernel. If the kernel is built
+with version 2, it will think the ``sizeof(struct foo)`` is 16.
+
+The kernel can use the ``_IOC_SIZE`` macro to get the size encoded
+in the ioctl code that the user passed in and then use
+``copy_struct_from_user()`` to safely copy the value::
+
+    int handle_ioctl(unsigned long cmd, unsigned long arg)
+    {
+        switch _IOC_NR(cmd) {
+        0x01: {
+            struct foo my_cmd;  /* size 16 in the kernel */
+
+            ret = copy_struct_from_user(&my_cmd, arg, sizeof(struct foo), _IOC_SIZE(cmd));
+            ...
+
+``copy_struct_from_user`` will zero the struct in the kernel and then copy
+only the bytes passed in from the user (leaving new members zeroized).
+If the user passed in a larger struct, the extra members are ignored.
+
+If you know this situation is accounted for in the kernel code, you can
+pass ``-i`` to the script, and struct expansions like this will be ignored.
+
+Flex Array Migration
+--------------------
+
+While the script handles expansion into an existing flex array, it does
+still flag initial migration to flex arrays from 1-element fake flex
+arrays. For example::
+
+    struct foo {
+          __u32 x;
+    -     __u32 flex[1]; /* fake flex */
+    +     __u32 flex[];  /* real flex */
+    };
+
+This change would be flagged by the script::
+
+    [C] 'struct foo' changed:
+      type size changed from 64 to 32 (in bits)
+      1 data member change:
+        type of '__u32 flex[1]' changed:
+          type name changed from '__u32[1]' to '__u32[]'
+          array type size changed from 32 to 'unknown'
+          array type subrange 1 changed length from 1 to 'unknown'
+
+At this time, there's no way to filter these types of changes, so be
+aware of this possible false positive.
+
+Summary
+-------
+
+While many types of false positives are filtered out by the script,
+it's possible there are some cases where the script flags a change
+which does not break UAPI. It's also possible a change which *does*
+break userspace would not be flagged by this script. While the script
+has been run on much of the kernel history, there could still be corner
+cases that are not accounted for.
+
+The intention is for this script to be used as a quick check for
+maintainers or automated tooling, not as the end-all authority on
+patch compatibility. It's best to remember: use your best judgment
+(and ideally a unit test in userspace) to make sure your UAPI changes
+are backwards-compatible!
diff --git a/Documentation/dev-tools/index.rst b/Documentation/dev-tools/index.rst
index 6b0663075dc0..0876f5a2cf55 100644
--- a/Documentation/dev-tools/index.rst
+++ b/Documentation/dev-tools/index.rst
@@ -34,6 +34,7 @@ Documentation/dev-tools/testing-overview.rst
    kselftest
    kunit/index
    ktap
+   checkuapi


 .. only::  subproject and html
--
2.17.1


^ permalink raw reply	[flat|nested] 8+ messages in thread

* [PATCH v6 3/3] check-module-params: Introduce check-module-params.sh
  2023-10-27 19:30 [PATCH v6 0/3] Validating UAPI backwards compatibility John Moon
  2023-10-27 19:30 ` [PATCH v6 1/3] check-uapi: Introduce check-uapi.sh John Moon
  2023-10-27 19:30 ` [PATCH v6 2/3] docs: dev-tools: Add UAPI checker documentation John Moon
@ 2023-10-27 19:30 ` John Moon
  2023-11-14 13:28   ` Masahiro Yamada
  2 siblings, 1 reply; 8+ messages in thread
From: John Moon @ 2023-10-27 19:30 UTC (permalink / raw)
  To: Masahiro Yamada, Nathan Chancellor, Nick Desaulniers,
	Nicolas Schier, Jonathan Corbet
  Cc: John Moon, linux-kbuild, linux-kernel, linux-arm-kernel,
	linux-arm-msm, kernel, Greg Kroah-Hartman, Randy Dunlap,
	Arnd Bergmann, Bjorn Andersson, Todd Kjos, Matthias Maennich,
	Giuliano Procida, kernel-team, libabigail, Dodji Seketeli,
	Trilok Soni, Satya Durga Srinivasu Prabhala, Jordan Crouse

One part of maintaining backwards compatibility with older
userspace programs is avoiding changes to module parameters.

To that end, add a script (check-module-params.sh) which
performs a simple check of module parameter changes across
git references.

For example, if this module parameter:

module_param(max_nullfunc_tries, int, 0644);

...restricted its mode parameter:

module_param(max_nullfunc_tries, int, 0600);

The script would flag the change:

Module parameter "max_nullfunc_tries" in net/mac80211/mlme.c changed!
  Original args: int,0644
       New args: int,0600

Signed-off-by: John Moon <quic_johmoo@quicinc.com>
---
 scripts/check-module-params.sh | 295 +++++++++++++++++++++++++++++++++
 1 file changed, 295 insertions(+)
 create mode 100755 scripts/check-module-params.sh

diff --git a/scripts/check-module-params.sh b/scripts/check-module-params.sh
new file mode 100755
index 000000000000..4d2b2cd483e8
--- /dev/null
+++ b/scripts/check-module-params.sh
@@ -0,0 +1,295 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0-only
+# Script to check commits for UAPI backwards compatibility
+
+set -o errexit
+set -o pipefail
+
+print_usage() {
+	name=$(basename "$0")
+	cat << EOF
+$name - check for module parameters stability across git commits.
+
+By default, the script will check to make sure the latest commit (or current
+dirty changes) did not introduce changes when compared to HEAD^1. You can
+check against additional commit ranges with the -b and -p options.
+
+Usage: $name [-b BASE_REF] [-p PAST_REF] [-j N] [-l ERROR_LOG] [-q] [-v]
+
+Options:
+    -b BASE_REF    Base git reference to use for comparison. If unspecified or empty,
+                   will use any dirty changes in tree to UAPI files. If there are no
+                   dirty changes, HEAD will be used.
+    -p PAST_REF    Compare BASE_REF to PAST_REF (e.g. -p v6.1). If unspecified or empty,
+                   will use BASE_REF^1. Must be an ancestor of BASE_REF. Only headers
+                   that exist on PAST_REF will be checked for compatibility.
+    -j JOBS        Number of checks to run in parallel (default: number of CPU cores).
+    -l ERROR_LOG   Write error log to file (default: no error log is generated).
+    -q             Quiet operation (suppress stdout, still print stderr).
+    -v             Verbose operation (print more information about each header being checked).
+
+Exit codes:
+    $SUCCESS) Success
+    $FAILURE) Module param differences detected
+EOF
+}
+
+readonly SUCCESS=0
+readonly FAILURE=1
+
+# Print to stderr
+eprintf() {
+	# shellcheck disable=SC2059
+	printf "$@" >&2
+}
+
+# Check if git tree is dirty
+tree_is_dirty() {
+	! git diff --quiet
+}
+
+file_module_params_unmodified() {
+	local file="$1"
+	local base_ref="$2"
+	local past_ref="$3"
+	local base_params_file="${TMP_DIR}/${file}.base"
+	local past_params_file="${TMP_DIR}/${file}.past"
+	local error_log="${TMP_DIR}/${file}.error"
+
+	local -r awk_cmd='/^ *module_param.*\(/,/.*\);/'
+
+	mkdir -p "$(dirname "$error_log")"
+	git show "${past_ref}:${file}" 2> /dev/null \
+		| awk "$awk_cmd" > "$past_params_file" || true
+
+	# Ignore files that don't exist at the past ref or don't have module params
+	if [ ! -s "$past_params_file" ]; then
+		return 255 # Special return code for "no-op"
+	fi
+
+	if [ -z "$base_ref" ]; then
+		awk "$awk_cmd" "${KERNEL_SRC}/${file}" \
+			> "$base_params_file" 2> /dev/null || true
+	else
+		git show "${base_ref}:${file}" 2> /dev/null \
+			| awk "$awk_cmd" > "$base_params_file" || true
+	fi
+
+	# Process the param data to come up with an associative array of param names to param data
+	# For example:
+	#   module_param_call(foo, set_result, get_result, NULL, 0600);
+	#
+	# is processed into:
+	#   pre_change_params[foo]="set_result,get_result,NULL,0600"
+	local -A pre_change_params
+	local param_name
+	local param_params
+	while read -r mod_param_args; do
+		param_name="$(echo "$mod_param_args" | cut -d ',' -f 1)"
+		param_params="$(echo "$mod_param_args" | cut -d ',' -f 2-)"
+
+		pre_change_params[$param_name]=$param_params
+	done < <(tr -d '\t\n ' < "$past_params_file" | tr ';' '\n' | grep -o '(.*)' | tr -d '()')
+
+	local -A post_change_params
+	while read -r mod_param_args; do
+		param_name="$(echo "$mod_param_args" | cut -d ',' -f 1)"
+		param_params="$(echo "$mod_param_args" | cut -d ',' -f 2-)"
+
+		post_change_params[$param_name]=$param_params
+	done < <(tr -d '\t\n ' < "$base_params_file" | tr ';' '\n' | grep -o '(.*)' | tr -d '()')
+
+	# Flag any module param changes that:
+	#  - Remove/rename a parameter
+	#  - Change the arguments of the parameter
+	local incompat_param_changes=0
+	local pre
+	local post
+	for param_name in "${!pre_change_params[@]}"; do
+		pre="${pre_change_params[$param_name]}"
+		if [ ! "${post_change_params[$param_name]+set}" ]; then
+			{
+				printf "Module parameter \"%s\" in %s removed!\n" "$param_name" "$file"
+				printf "  Original args: %s\n" "$pre"
+			} > "$error_log"
+			incompat_param_changes=$((incompat_param_changes + 1))
+			continue
+		fi
+
+		post="${post_change_params[$param_name]}"
+		if [ "$pre" != "$post" ]; then
+			{
+				printf "Module parameter \"%s\" in %s changed!\n" "$param_name" "$file"
+				printf "  Original args: %s\n" "$pre"
+				printf "       New args: %s\n" "$post"
+			} > "$error_log"
+			incompat_param_changes=$((incompat_param_changes + 1))
+			continue
+		fi
+	done
+
+	if [ "$incompat_param_changes" -gt 0 ]; then
+		return 1
+	fi
+}
+
+run() {
+	local base_ref="$1"
+	local past_ref="$2"
+	local abi_error_log="$3"
+
+	diff_args=("$past_ref")
+	if [ -n "$base_ref" ]; then
+		diff_args+=("$base_ref")
+	fi
+
+	local -a threads=()
+	local passed=0
+	local failed=0
+	printf "Checking files between %s and %s for module parameter compatibility...\n" \
+		"$past_ref" "$base_ref"
+	while read -r modified_file; do
+		if [ "${#threads[@]}" -ge "$MAX_THREADS" ]; then
+			wait "${threads[0]}" && ret="$?" || ret="$?"
+			if [ "$ret" -eq 0 ]; then
+				passed=$((passed + 1))
+			elif [ "$ret" -eq 1 ]; then
+				failed=$((failed + 1))
+			fi
+			threads=("${threads[@]:1}")
+		fi
+
+		file_module_params_unmodified "$modified_file" "$base_ref" "$past_ref" &
+		threads+=("$!")
+	done < <(git diff --diff-filter=MCD --name-only "${diff_args[@]}" -- '*.c' '*.h')
+
+	for t in "${threads[@]}"; do
+		wait "$t" && ret="$?" || ret="$?"
+		if [ "$ret" -eq 0 ]; then
+			passed=$((passed + 1))
+		elif [ "$ret" -eq 1 ]; then
+			failed=$((failed + 1))
+		fi
+	done
+
+	total=$((passed + failed))
+	if [ "$total" -eq 0 ]; then
+		printf "No files with module parameters modified between %s and %s\n" \
+			"$past_ref" "${base_ref:-dirty tree}"
+		exit 0
+	fi
+
+	if [ -n "$abi_error_log" ]; then
+		printf 'Generated by "%s %s" from git ref %s\n\n' \
+			"$0" "$*" "$(git rev-parse HEAD)" > "$abi_error_log"
+	fi
+
+	while read -r error_file; do
+		{
+			cat "$error_file"
+			printf "\n\n"
+		} | tee -a "${abi_error_log:-/dev/null}" >&2
+	done < <(find "$TMP_DIR" -type f -name '*.error')
+
+	if [ "$failed" -gt 0 ]; then
+		eprintf "error - %d/%d files with modules parameters appear _not_ to be backwards compatible\n" \
+			"$failed" "$total"
+		if [ -n "$abi_error_log" ]; then
+			eprintf "Failure summary saved to %s\n" "$abi_error_log"
+		fi
+	else
+		printf "All %d files with module_parameters checked appear to be backwards compatible\n" \
+			"$total" "$ARCH"
+	fi
+
+	exit "$failed"
+}
+
+# Make sure the git refs we have make sense
+check_refs() {
+	if ! git rev-parse --is-inside-work-tree > /dev/null 2>&1; then
+		eprintf "error - this script requires the kernel tree to be initialized with Git\n"
+		return 1
+	fi
+
+	if ! git rev-parse --verify "$past_ref" > /dev/null 2>&1; then
+		printf 'error - invalid git reference "%s"\n' "$past_ref"
+		return 1
+	fi
+
+	if [ -n "$base_ref" ]; then
+		if ! git merge-base --is-ancestor "$past_ref" "$base_ref" > /dev/null 2>&1; then
+			printf 'error - "%s" is not an ancestor of base ref "%s"\n' "$past_ref" "$base_ref"
+			return 1
+		fi
+		if [ "$(git rev-parse "$base_ref")" = "$(git rev-parse "$past_ref")" ]; then
+			printf 'error - "%s" and "%s" are the same reference\n' "$past_ref" "$base_ref"
+			return 1
+		fi
+	fi
+}
+
+main() {
+	MAX_THREADS=$(nproc)
+	quiet="false"
+	local base_ref=""
+	while getopts "hb:p:j:l:q" opt; do
+		case $opt in
+		h)
+			print_usage
+			exit "$SUCCESS"
+			;;
+		b)
+			base_ref="$OPTARG"
+			;;
+		p)
+			past_ref="$OPTARG"
+			;;
+		j)
+			MAX_THREADS="$OPTARG"
+			;;
+		l)
+			abi_error_log="$OPTARG"
+			;;
+		q)
+			quiet="true"
+			;;
+		*)
+			exit "$FAIL_PREREQ"
+		esac
+	done
+
+	if [ "$quiet" = "true" ]; then
+		exec > /dev/null 2>&1
+	fi
+
+	if [ -z "$KERNEL_SRC" ]; then
+		KERNEL_SRC="$(realpath "$(dirname "$0")"/..)"
+	fi
+
+	cd "$KERNEL_SRC"
+
+	if [ -z "$base_ref" ] && ! tree_is_dirty; then
+		base_ref=HEAD
+	fi
+
+	if [ -z "$past_ref" ]; then
+		if [ -n "$base_ref" ]; then
+			past_ref="${base_ref}^1"
+		else
+			past_ref=HEAD
+		fi
+	fi
+
+	if ! check_refs; then
+		exit "$FAIL_PREREQ"
+	fi
+
+	TMP_DIR=$(mktemp -d)
+	readonly TMP_DIR
+	trap 'rm -rf "$TMP_DIR"' EXIT
+
+	run "$base_ref" "$past_ref" "$abi_error_log"
+}
+
+main "$@"
--
2.17.1


^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: [PATCH v6 1/3] check-uapi: Introduce check-uapi.sh
  2023-10-27 19:30 ` [PATCH v6 1/3] check-uapi: Introduce check-uapi.sh John Moon
@ 2023-11-14 10:10   ` Masahiro Yamada
  2023-11-14 12:24     ` Greg Kroah-Hartman
  2023-12-11 22:30     ` John Moon
  0 siblings, 2 replies; 8+ messages in thread
From: Masahiro Yamada @ 2023-11-14 10:10 UTC (permalink / raw)
  To: John Moon, Greg Kroah-Hartman
  Cc: Nathan Chancellor, Nick Desaulniers, Nicolas Schier,
	Jonathan Corbet, linux-kbuild, linux-kernel, linux-arm-kernel,
	linux-arm-msm, kernel, Randy Dunlap, Arnd Bergmann,
	Bjorn Andersson, Todd Kjos, Matthias Maennich, Giuliano Procida,
	kernel-team, libabigail, Dodji Seketeli, Trilok Soni,
	Satya Durga Srinivasu Prabhala, Jordan Crouse

On Sat, Oct 28, 2023 at 4:31 AM John Moon <quic_johmoo@quicinc.com> wrote:
>
> While the kernel community has been good at maintaining backwards
> compatibility with kernel UAPIs, it would be helpful to have a tool
> to check if a commit introduces changes that break backwards
> compatibility.
>
> To that end, introduce check-uapi.sh: a simple shell script that
> checks for changes to UAPI headers using libabigail.
>
> libabigail is "a framework which aims at helping developers and
> software distributors to spot some ABI-related issues like interface
> incompatibility in ELF shared libraries by performing a static
> analysis of the ELF binaries at hand."
>
> The script uses one of libabigail's tools, "abidiff", to compile the
> changed header before and after the commit to detect any changes.
>
> abidiff "compares the ABI of two shared libraries in ELF format. It
> emits a meaningful report describing the differences between the two
> ABIs."
>
> The script also includes the ability to check the compatibility of
> all UAPI headers across commits. This allows developers to inspect
> the stability of the UAPIs over time.
>
> Signed-off-by: John Moon <quic_johmoo@quicinc.com>
> ---
>     - Add abidiff suppressions to filter out common things like enum
>       variants named .*_MAX being changed and expansion into padding
>       fields.
>     - Bump minimum abidiff version to 2.4 to accomodate new
>       suppressions.
>     - Add option (-i) to suppress ambiguous breaking changes.
>     - Remove printing of full file diffs when ABI breakage is found
>       as this was too noisy.
>     - Wait for all files to be checked before printing results as
>       printing from parallel threads was garbling output.
>     - Suppress all output when -q is passed.
>     - Avoid messing up user's git tree by using "git archive" instead
>       of checking out references.



The code looks almost good to me.

(I left some more comments below, but they are minor).



Greg,
Could you check the output from the tool?
Is it OK with you?












>
>  scripts/check-uapi.sh | 585 ++++++++++++++++++++++++++++++++++++++++++
>  1 file changed, 585 insertions(+)
>  create mode 100755 scripts/check-uapi.sh
>
> diff --git a/scripts/check-uapi.sh b/scripts/check-uapi.sh
> new file mode 100755
> index 000000000000..bcb03c8dae60
> --- /dev/null
> +++ b/scripts/check-uapi.sh
> @@ -0,0 +1,585 @@
> +#!/bin/bash
> +# SPDX-License-Identifier: GPL-2.0-only
> +# Script to check commits for UAPI backwards compatibility
> +
> +set -o errexit
> +set -o pipefail
> +
> +print_usage() {
> +       name=$(basename "$0")
> +       cat << EOF
> +$name - check for UAPI header stability across Git commits
> +
> +By default, the script will check to make sure the latest commit (or current
> +dirty changes) did not introduce ABI changes when compared to HEAD^1. You can
> +check against additional commit ranges with the -b and -p options.
> +
> +The script will not check UAPI headers for architectures other than the one
> +defined in ARCH.
> +
> +Usage: $name [-b BASE_REF] [-p PAST_REF] [-j N] [-l ERROR_LOG] [-i] [-q] [-v]
> +
> +Options:
> +    -b BASE_REF    Base git reference to use for comparison. If unspecified or empty,
> +                   will use any dirty changes in tree to UAPI files. If there are no
> +                   dirty changes, HEAD will be used.
> +    -p PAST_REF    Compare BASE_REF to PAST_REF (e.g. -p v6.1). If unspecified or empty,
> +                   will use BASE_REF^1. Must be an ancestor of BASE_REF. Only headers
> +                   that exist on PAST_REF will be checked for compatibility.
> +    -j JOBS        Number of checks to run in parallel (default: number of CPU cores).
> +    -l ERROR_LOG   Write error log to file (default: no error log is generated).
> +    -i             Ignore ambiguous changes that may or may not break UAPI compatibility.
> +    -q             Quiet operation.
> +    -v             Verbose operation (print more information about each header being checked).
> +
> +Environmental args:
> +    ABIDIFF  Custom path to abidiff binary
> +    CC       C compiler (default is "gcc")
> +    ARCH     Target architecture of C compiler (default is host arch)



ARCH is not the arch of C compiler.
It is the arch for the UAPI check.

It is better to reword it.






> +
> +Exit codes:
> +    $SUCCESS) Success
> +    $FAIL_ABI) ABI difference detected
> +    $FAIL_PREREQ) Prerequisite not met
> +EOF
> +}
> +
> +readonly SUCCESS=0
> +readonly FAIL_ABI=1
> +readonly FAIL_PREREQ=2
> +
> +# Print to stderr
> +eprintf() {
> +       # shellcheck disable=SC2059
> +       printf "$@" >&2
> +}
> +
> +# Expand an array with a specific character (similar to Python string.join())
> +join() {
> +       local IFS="$1"
> +       shift
> +       printf "%s" "$*"
> +}
> +
> +# Create abidiff suppressions
> +gen_suppressions() {
> +       # Common enum variant names which we don't want to worry about
> +       # being shifted when new variants are added.
> +       local -a enum_regex=(
> +               ".*_AFTER_LAST$"
> +               ".*_CNT$"
> +               ".*_COUNT$"
> +               ".*_END$"
> +               ".*_LAST$"
> +               ".*_MASK$"
> +               ".*_MAX$"
> +               ".*_MAX_BIT$"
> +               ".*_MAX_BPF_ATTACH_TYPE$"
> +               ".*_MAX_ID$"
> +               ".*_MAX_SHIFT$"
> +               ".*_NBITS$"
> +               ".*_NETDEV_NUMHOOKS$"
> +               ".*_NFT_META_IIFTYPE$"
> +               ".*_NL80211_ATTR$"
> +               ".*_NLDEV_NUM_OPS$"
> +               ".*_NUM$"
> +               ".*_NUM_ELEMS$"
> +               ".*_NUM_IRQS$"
> +               ".*_SIZE$"
> +               ".*_TLSMAX$"
> +               "^MAX_.*"
> +               "^NUM_.*"
> +       )
> +
> +       # Common padding field names which can be expanded into
> +       # without worrying about users.
> +       local -a padding_regex=(
> +               ".*end$"
> +               ".*pad$"
> +               ".*pad[0-9]?$"
> +               ".*pad_[0-9]?$"
> +               ".*padding$"
> +               ".*padding[0-9]?$"
> +               ".*padding_[0-9]?$"
> +               ".*res$"
> +               ".*resv$"
> +               ".*resv[0-9]?$"
> +               ".*resv_[0-9]?$"
> +               ".*reserved$"
> +               ".*reserved[0-9]?$"
> +               ".*reserved_[0-9]?$"
> +               ".*rsvd[0-9]?$"
> +               ".*unused$"
> +       )
> +
> +       cat << EOF
> +[suppress_type]
> +  type_kind = enum
> +  changed_enumerators_regexp = $(join , "${enum_regex[@]}")
> +EOF
> +
> +       for p in "${padding_regex[@]}"; do
> +               cat << EOF
> +[suppress_type]
> +  type_kind = struct
> +  has_data_member_inserted_at = offset_of_first_data_member_regexp(${p})
> +EOF
> +       done
> +
> +if [ "$IGNORE_AMBIGUOUS_CHANGES" = "true" ]; then
> +       cat << EOF
> +[suppress_type]
> +  type_kind = struct
> +  has_data_member_inserted_at = end
> +  has_size_change = yes
> +EOF
> +fi
> +}
> +
> +# Check if git tree is dirty
> +tree_is_dirty() {
> +       ! git diff --quiet
> +}
> +
> +# Get list of files installed in $ref
> +get_file_list() {
> +       local -r ref="$1"
> +       local -r tree="$(get_header_tree "$ref")"
> +
> +       # Print all installed headers, filtering out ones that can't be compiled
> +       find "$tree" -type f -name '*.h' -printf '%P\n' | grep -v -f "$INCOMPAT_LIST"
> +}
> +
> +# Add to the list of incompatible headers
> +add_to_incompat_list() {
> +       local -r ref="$1"
> +
> +       # Start with the usr/include/Makefile to get a list of the headers
> +       # that don't compile using this method.
> +       if [ ! -f usr/include/Makefile ]; then
> +               eprintf "error - no usr/include/Makefile present at %s\n" "$ref"
> +               eprintf "Note: usr/include/Makefile was added in the v5.3 kernel release\n"
> +               exit "$FAIL_PREREQ"
> +       fi
> +       {
> +               # shellcheck disable=SC2016
> +               printf 'all: ; @echo $(no-header-test)\n'
> +               cat usr/include/Makefile
> +       } | SRCARCH="$ARCH" make --always-make -f - | tr " " "\n" \
> +         | grep -v "asm-generic" >> "$INCOMPAT_LIST"
> +
> +       # The makefile also skips all asm-generic files, but prints "asm-generic/%"
> +       # which won't work for our grep match. Instead, print something grep will match.
> +       printf "asm-generic/.*\.h\n" >> "$INCOMPAT_LIST"
> +
> +       sort -u -o "$INCOMPAT_LIST" "$INCOMPAT_LIST"
> +       sed -i -e '/^$/d' "$INCOMPAT_LIST"



Maybe, you can move these two lines to the
end of install_headers().

The reason for the duplication is because
run_make_headers_install() is executed twice.

You do not need to sort INCOMPAT_LIST
in the first run of run_make_headers_install().





> +}
> +
> +# Compile the simple test app
> +do_compile() {
> +       local -r inc_dir="$1"
> +       local -r header="$2"
> +       local -r out="$3"
> +       printf "int main(void) { return 0; }\n" | \
> +               "$CC" -c \
> +                 -o "$out" \
> +                 -x c \
> +                 -O0 \
> +                 -std=c90 \
> +                 -fno-eliminate-unused-debug-types \
> +                 -g \
> +                 "-I${inc_dir}" \
> +                 -include "$header" \
> +                 -
> +}
> +
> +# Run make headers_install
> +run_make_headers_install() {
> +       local -r install_dir="$1"



You can call get_header_tree() here,
and pass "$ref" to this function.





> +       make -j "$MAX_THREADS" ARCH="$ARCH" INSTALL_HDR_PATH="$install_dir" \
> +               headers_install > /dev/null
> +}
> +
> +# Install headers for both git refs
> +install_headers() {
> +       local -r base_ref="$1"
> +       local -r past_ref="$2"
> +
> +       for ref in "$base_ref" "$past_ref"; do
> +               printf "Installing user-facing UAPI headers from %s... " "${ref:-dirty tree}"
> +               if [ -n "$ref" ]; then
> +                       git archive --format=tar --prefix="${ref}-archive/" "$ref" \
> +                               | (cd "$TMP_DIR" && tar xf -)
> +                       (
> +                               cd "${TMP_DIR}/${ref}-archive"
> +                               run_make_headers_install "${TMP_DIR}/${ref}/usr"
> +                               add_to_incompat_list "$ref" "$INCOMPAT_LIST"
> +                       )
> +               else
> +                       run_make_headers_install "${TMP_DIR}/${ref}/usr"
> +                       add_to_incompat_list "$ref" "$INCOMPAT_LIST"



If we drop the dirty tree support, we can make the code simpler.

$ref would never become empty, and we would always be able to run
"make headers_install" in the /tmp directory.

("make headers_install" in the working tree would create intermediate
build artifacts in usr/include/)

That is my personal opinion.


If you insist on the dirty tree support,
I will leave it to you.







> +               fi
> +               printf "OK\n"
> +       done
> +}
> +
> +# Print the path to the headers_install tree for a given ref
> +get_header_tree() {
> +       local -r ref="$1"
> +       printf "%s" "${TMP_DIR}/${ref}/usr"
> +}
> +
> +# Check file list for UAPI compatibility
> +check_uapi_files() {
> +       local -r base_ref="$1"
> +       local -r past_ref="$2"
> +       local -r abi_error_log="$3"
> +
> +       local passed=0;
> +       local failed=0;
> +       local -a threads=()
> +       set -o errexit
> +
> +       printf "Checking changes to UAPI headers between %s and %s...\n" "$past_ref" "${base_ref:-dirty tree}"
> +       # Loop over all UAPI headers that were installed by $past_ref (if they only exist on $base_ref,
> +       # there's no way they're broken and no way to compare anyway)
> +       while read -r file; do
> +               if [ "${#threads[@]}" -ge "$MAX_THREADS" ]; then
> +                       if wait "${threads[0]}"; then
> +                               passed=$((passed + 1))
> +                       else
> +                               failed=$((failed + 1))
> +                       fi
> +                       threads=("${threads[@]:1}")
> +               fi
> +
> +               check_individual_file "$base_ref" "$past_ref" "$file" &
> +               threads+=("$!")
> +       done < <(get_file_list "$past_ref")
> +
> +       for t in "${threads[@]}"; do
> +               if wait "$t"; then
> +                       passed=$((passed + 1))
> +               else
> +                       failed=$((failed + 1))
> +               fi
> +       done
> +
> +       if [ -n "$abi_error_log" ]; then
> +               printf 'Generated by "%s %s" from git ref %s\n\n' \
> +                       "$0" "$*" "$(git rev-parse HEAD)" > "$abi_error_log"
> +       fi
> +
> +       while read -r error_file; do
> +               {
> +                       cat "$error_file"
> +                       printf "\n\n"
> +               } | tee -a "${abi_error_log:-/dev/null}" >&2
> +       done < <(find "$TMP_DIR" -type f -name '*.error')



Does this produce this deterministic result?


Maybe, adding '| sort' after the 'find' command
will produce the sorted report?













> +
> +       total="$((passed + failed))"
> +       if [ "$failed" -gt 0 ]; then
> +               eprintf "error - %d/%d UAPI headers compatible with %s appear _not_ to be backwards compatible\n" \
> +                       "$failed" "$total" "$ARCH"
> +               if [ -n "$abi_error_log" ]; then
> +                       eprintf "Failure summary saved to %s\n" "$abi_error_log"
> +               fi
> +       else
> +               printf "All %d UAPI headers compatible with %s appear to be backwards compatible\n" \
> +                       "$total" "$ARCH"
> +       fi
> +
> +       return "$failed"
> +}
> +
> +# Check an individual file for UAPI compatibility
> +check_individual_file() {
> +       local -r base_ref="$1"
> +       local -r past_ref="$2"
> +       local -r file="$3"
> +
> +       local -r base_header="$(get_header_tree "$base_ref")/${file}"
> +       local -r past_header="$(get_header_tree "$past_ref")/${file}"
> +
> +       local base_ref_short="${base_ref:-dirty tree}"
> +       if [ "${#base_ref_short}" -ge 40 ]; then
> +               base_ref_short="$(git rev-parse --short "$base_ref_short")"



I believe this is intended to keep the log short, but
I think we can simply use the ref name passed by the user.
If ${ref} is crazily long, it is a user's fault.







> +       fi
> +
> +       local past_ref_short="$past_ref"
> +       if [ "${#past_ref_short}" -ge 40 ]; then
> +               past_ref_short="$(git rev-parse --short "$past_ref_short")"
> +       fi
> +
> +       if [ ! -f "$base_header" ]; then
> +               mkdir -p "$(dirname "$base_header")"
> +               printf "==== UAPI header %s was removed between %s and %s ====" \
> +                       "$file" "$past_ref_short" "$base_ref_short" \
> +                               > "${base_header}.error"
> +               return 1
> +       fi
> +
> +       compare_abi "$file" "$base_header" "$past_header" "$base_ref" "$past_ref" \
> +               "$base_ref_short" "$past_ref_short"
> +}
> +
> +# Perform the A/B compilation and compare output ABI
> +compare_abi() {
> +       local -r file="$1"
> +       local -r base_header="$2"
> +       local -r past_header="$3"
> +       local -r base_ref="$4"
> +       local -r past_ref="$5"
> +       local -r base_ref_short="$6"
> +       local -r past_ref_short="$7"
> +       local -r log="${TMP_DIR}/log/${file}.log"
> +
> +       mkdir -p "$(dirname "$log")"
> +
> +       if ! do_compile "$(get_header_tree "$base_ref")/include" "$base_header" "${base_header}.bin" 2> "$log"; then
> +               {
> +                       warn_str=$(printf "==== Could not compile version of UAPI header %s at %s ====\n" \
> +                               "$file" "$base_ref_short")
> +                       printf "%s\n" "$warn_str"
> +                       cat "$log"
> +                       printf -- "=%.0s" $(seq 0 ${#warn_str})
> +               } > "${base_header}.error"
> +               return 1
> +       fi
> +
> +       if ! do_compile "$(get_header_tree "$past_ref")/include" "$past_header" "${past_header}.bin" 2> "$log"; then
> +               {
> +                       warn_str=$(printf "==== Could not compile version of UAPI header %s at %s ====\n" \
> +                               "$file" "$past_ref_short")
> +                       printf "%s\n" "$warn_str"
> +                       cat "$log"
> +                       printf -- "=%.0s" $(seq 0 ${#warn_str})
> +               } > "${base_header}.error"



You used  >"${base_header}.error" instead of
>"${past_header}.error" here.

I guess it is intentional.



But, I think you can put *.error files
under the ${TMP_DIR}/log/ directory if you save the log
from base_ref and past_ref into the same log file.


For example,


   local -r error_log="${TMP_DIR}/log/${file}.error"


Then,

    {
          ...

    } > "${error_log}"










> +               return 1
> +       fi
> +
> +       local ret=0
> +       "$ABIDIFF" --non-reachable-types \
> +               --suppressions "$SUPPRESSIONS" \
> +               "${past_header}.bin" "${base_header}.bin" > "$log" || ret="$?"
> +       if [ "$ret" -eq 0 ]; then
> +               if [ "$VERBOSE" = "true" ]; then
> +                       printf "No ABI differences detected in %s from %s -> %s\n" \
> +                               "$file" "$past_ref_short" "$base_ref_short"
> +               fi
> +       else
> +               # Bits in abidiff's return code can be used to determine the type of error
> +               if [ $((ret & 0x2)) -gt 0 ]; then
> +                       eprintf "error - abidiff did not run properly\n"
> +                       exit 1
> +               fi
> +
> +               if [ "$IGNORE_AMBIGUOUS_CHANGES" = "true" ] && [ "$ret" -eq 4 ]; then
> +                       return 0
> +               fi
> +
> +               # If the only changes were additions (not modifications to existing APIs), then
> +               # there's no problem. Ignore these diffs.
> +               if grep "Unreachable types summary" "$log" | grep -q "0 removed" &&
> +                  grep "Unreachable types summary" "$log" | grep -q "0 changed"; then
> +                       return 0
> +               fi
> +
> +               {
> +                       warn_str=$(printf "==== ABI differences detected in %s from %s -> %s ====" \
> +                               "$file" "$past_ref_short" "$base_ref_short")
> +                       printf "%s\n" "$warn_str"
> +                       sed  -e '/summary:/d' -e '/changed type/d' -e '/^$/d' -e 's/^/  /g' "$log"
> +                       printf -- "=%.0s" $(seq 0 ${#warn_str})
> +                       if cmp "$past_header" "$base_header" > /dev/null 2>&1; then
> +                               printf "\n%s did not change between %s and %s...\n" "$file" "$past_ref" "${base_ref:-dirty tree}"
> +                               printf "It's possible a change to one of the headers it includes caused this error:\n"
> +                               grep '^#include' "$base_header"
> +                               printf "\n"
> +                       fi
> +               } > "${base_header}.error"
> +
> +               return 1
> +       fi
> +}
> +
> +# Check that a minimum software version number is satisfied
> +min_version_is_satisfied() {
> +       local -r min_version="$1"
> +       local -r version_installed="$2"
> +
> +       printf "%s\n%s\n" "$min_version" "$version_installed" \
> +               | sort -Vc > /dev/null 2>&1
> +}
> +
> +# Make sure we have the tools we need and the arguments make sense
> +check_deps() {
> +       ABIDIFF="${ABIDIFF:-abidiff}"
> +       CC="${CC:-gcc}"
> +       ARCH="${ARCH:-$(uname -m)}"
> +       if [ "$ARCH" = "x86_64" ]; then
> +               ARCH="x86"
> +       fi
> +
> +       local -r abidiff_min_version="2.4"
> +       local -r libdw_min_version_if_clang="0.171"
> +
> +       if ! command -v "$ABIDIFF" > /dev/null 2>&1; then
> +               eprintf "error - abidiff not found!\n"
> +               eprintf "Please install abigail-tools version %s or greater\n" "$abidiff_min_version"
> +               eprintf "See: https://sourceware.org/libabigail/manual/libabigail-overview.html\n"
> +               return 1
> +       fi
> +
> +       local -r abidiff_version="$("$ABIDIFF" --version | cut -d ' ' -f 2)"
> +       if ! min_version_is_satisfied "$abidiff_min_version" "$abidiff_version"; then
> +               eprintf "error - abidiff version too old: %s\n" "$abidiff_version"
> +               eprintf "Please install abigail-tools version %s or greater\n" "$abidiff_min_version"
> +               eprintf "See: https://sourceware.org/libabigail/manual/libabigail-overview.html\n"
> +               return 1
> +       fi
> +
> +       if ! command -v "$CC" > /dev/null 2>&1; then
> +               eprintf 'error - %s not found\n' "$CC"
> +               return 1
> +       fi
> +
> +       if "$CC" --version | grep -q clang; then
> +               local -r libdw_version="$(ldconfig -v 2>/dev/null | grep -v SKIPPED | grep -m 1 -o 'libdw-[0-9]\+.[0-9]\+' | cut -c 7-)"
> +               if ! min_version_is_satisfied "$libdw_min_version_if_clang" "$libdw_version"; then
> +                       eprintf "error - libdw version too old for use with clang: %s\n" "$libdw_version"
> +                       eprintf "Please install libdw from elfutils version %s or greater\n" "$libdw_min_version_if_clang"
> +                       eprintf "See: https://sourceware.org/elfutils/\n"
> +                       return 1
> +               fi
> +       fi
> +
> +       if [ ! -d "arch/${ARCH}" ]; then
> +               eprintf 'error - ARCH "%s" is not a subdirectory under arch/\n' "$ARCH"
> +               eprintf "Please set ARCH to one of:\n%s\n" "$(find arch -maxdepth 1 -mindepth 1 -type d -printf '%f ' | fmt)"
> +               return 1
> +       fi
> +
> +       if ! git rev-parse --is-inside-work-tree > /dev/null 2>&1; then
> +               eprintf "error - this script requires the kernel tree to be initialized with Git\n"
> +               return 1
> +       fi
> +
> +       if ! git rev-parse --verify "$past_ref" > /dev/null 2>&1; then
> +               printf 'error - invalid git reference "%s"\n' "$past_ref"
> +               return 1
> +       fi
> +
> +       if [ -n "$base_ref" ]; then
> +               if ! git merge-base --is-ancestor "$past_ref" "$base_ref" > /dev/null 2>&1; then
> +                       printf 'error - "%s" is not an ancestor of base ref "%s"\n' "$past_ref" "$base_ref"
> +                       return 1
> +               fi
> +               if [ "$(git rev-parse "$base_ref")" = "$(git rev-parse "$past_ref")" ]; then
> +                       printf 'error - "%s" and "%s" are the same reference\n' "$past_ref" "$base_ref"
> +                       return 1
> +               fi
> +       fi
> +}
> +
> +run() {
> +       local base_ref="$1"
> +       local past_ref="$2"
> +       local abi_error_log="$3"
> +       shift 3
> +
> +       if [ -z "$KERNEL_SRC" ]; then
> +               KERNEL_SRC="$(realpath "$(dirname "$0")"/..)"
> +       fi
> +
> +       cd "$KERNEL_SRC"
> +
> +       if [ -z "$base_ref" ] && ! tree_is_dirty; then
> +               base_ref=HEAD
> +       fi
> +
> +       if [ -z "$past_ref" ]; then
> +               if [ -n "$base_ref" ]; then
> +                       past_ref="${base_ref}^1"
> +               else
> +                       past_ref=HEAD
> +               fi
> +       fi
> +
> +       if ! check_deps; then
> +               exit "$FAIL_PREREQ"
> +       fi
> +
> +       TMP_DIR=$(mktemp -d)
> +       readonly TMP_DIR
> +       trap 'rm -rf "$TMP_DIR"' EXIT
> +
> +       readonly INCOMPAT_LIST="${TMP_DIR}/incompat_list.txt"
> +       touch "$INCOMPAT_LIST"
> +
> +       readonly SUPPRESSIONS="${TMP_DIR}/suppressions.txt"
> +       gen_suppressions > "$SUPPRESSIONS"
> +
> +       # Run make install_headers for both refs
> +       install_headers "$base_ref" "$past_ref"
> +
> +       # Check for any differences in the installed header trees
> +       if diff -r -q "$(get_header_tree "$base_ref")" "$(get_header_tree "$past_ref")" > /dev/null 2>&1; then
> +               printf "No changes to UAPI headers were applied between %s and %s\n" "$past_ref" "${base_ref:-dirty tree}"
> +               exit "$SUCCESS"
> +       fi
> +
> +       if ! check_uapi_files "$base_ref" "$past_ref" "$abi_error_log"; then
> +               exit "$FAIL_ABI"
> +       fi
> +}
> +
> +main() {
> +       MAX_THREADS=$(nproc)
> +       VERBOSE="false"
> +       IGNORE_AMBIGUOUS_CHANGES="false"
> +       quiet="false"
> +       local base_ref=""
> +       while getopts "hb:p:j:l:iqv" opt; do
> +               case $opt in
> +               h)
> +                       print_usage
> +                       exit "$SUCCESS"
> +                       ;;
> +               b)
> +                       base_ref="$OPTARG"
> +                       ;;
> +               p)
> +                       past_ref="$OPTARG"
> +                       ;;
> +               j)
> +                       MAX_THREADS="$OPTARG"
> +                       ;;
> +               l)
> +                       abi_error_log="$OPTARG"
> +                       ;;
> +               i)
> +                       IGNORE_AMBIGUOUS_CHANGES="true"
> +                       ;;
> +               q)
> +                       quiet="true"
> +                       VERBOSE="false"
> +                       ;;
> +               v)
> +                       VERBOSE="true"
> +                       quiet="false"
> +                       ;;
> +               *)
> +                       exit "$FAIL_PREREQ"
> +               esac
> +       done
> +
> +       if [ "$quiet" = "true" ]; then
> +               exec > /dev/null 2>&1
> +       fi
> +
> +       run "$base_ref" "$past_ref" "$abi_error_log" "$@"
> +}
> +
> +main "$@"
> --
> 2.17.1
>


--
Best Regards

Masahiro Yamada

^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: [PATCH v6 1/3] check-uapi: Introduce check-uapi.sh
  2023-11-14 10:10   ` Masahiro Yamada
@ 2023-11-14 12:24     ` Greg Kroah-Hartman
  2023-12-11 22:30     ` John Moon
  1 sibling, 0 replies; 8+ messages in thread
From: Greg Kroah-Hartman @ 2023-11-14 12:24 UTC (permalink / raw)
  To: Masahiro Yamada
  Cc: John Moon, Nathan Chancellor, Nick Desaulniers, Nicolas Schier,
	Jonathan Corbet, linux-kbuild, linux-kernel, linux-arm-kernel,
	linux-arm-msm, kernel, Randy Dunlap, Arnd Bergmann,
	Bjorn Andersson, Todd Kjos, Matthias Maennich, Giuliano Procida,
	kernel-team, libabigail, Dodji Seketeli, Trilok Soni,
	Satya Durga Srinivasu Prabhala, Jordan Crouse

On Tue, Nov 14, 2023 at 07:10:51PM +0900, Masahiro Yamada wrote:
> On Sat, Oct 28, 2023 at 4:31 AM John Moon <quic_johmoo@quicinc.com> wrote:
> >
> > While the kernel community has been good at maintaining backwards
> > compatibility with kernel UAPIs, it would be helpful to have a tool
> > to check if a commit introduces changes that break backwards
> > compatibility.
> >
> > To that end, introduce check-uapi.sh: a simple shell script that
> > checks for changes to UAPI headers using libabigail.
> >
> > libabigail is "a framework which aims at helping developers and
> > software distributors to spot some ABI-related issues like interface
> > incompatibility in ELF shared libraries by performing a static
> > analysis of the ELF binaries at hand."
> >
> > The script uses one of libabigail's tools, "abidiff", to compile the
> > changed header before and after the commit to detect any changes.
> >
> > abidiff "compares the ABI of two shared libraries in ELF format. It
> > emits a meaningful report describing the differences between the two
> > ABIs."
> >
> > The script also includes the ability to check the compatibility of
> > all UAPI headers across commits. This allows developers to inspect
> > the stability of the UAPIs over time.
> >
> > Signed-off-by: John Moon <quic_johmoo@quicinc.com>
> > ---
> >     - Add abidiff suppressions to filter out common things like enum
> >       variants named .*_MAX being changed and expansion into padding
> >       fields.
> >     - Bump minimum abidiff version to 2.4 to accomodate new
> >       suppressions.
> >     - Add option (-i) to suppress ambiguous breaking changes.
> >     - Remove printing of full file diffs when ABI breakage is found
> >       as this was too noisy.
> >     - Wait for all files to be checked before printing results as
> >       printing from parallel threads was garbling output.
> >     - Suppress all output when -q is passed.
> >     - Avoid messing up user's git tree by using "git archive" instead
> >       of checking out references.
> 
> 
> 
> The code looks almost good to me.
> 
> (I left some more comments below, but they are minor).
> 
> 
> 
> Greg,
> Could you check the output from the tool?

I will, give me a chance to catch up after the merge window and this
week at Plumbers....  Should be a week or so, thanks.

greg k-h

^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: [PATCH v6 3/3] check-module-params: Introduce check-module-params.sh
  2023-10-27 19:30 ` [PATCH v6 3/3] check-module-params: Introduce check-module-params.sh John Moon
@ 2023-11-14 13:28   ` Masahiro Yamada
  0 siblings, 0 replies; 8+ messages in thread
From: Masahiro Yamada @ 2023-11-14 13:28 UTC (permalink / raw)
  To: John Moon
  Cc: Nathan Chancellor, Nick Desaulniers, Nicolas Schier,
	Jonathan Corbet, linux-kbuild, linux-kernel, linux-arm-kernel,
	linux-arm-msm, kernel, Greg Kroah-Hartman, Randy Dunlap,
	Arnd Bergmann, Bjorn Andersson, Todd Kjos, Matthias Maennich,
	Giuliano Procida, kernel-team, libabigail, Dodji Seketeli,
	Trilok Soni, Satya Durga Srinivasu Prabhala, Jordan Crouse

On Sat, Oct 28, 2023 at 4:31 AM John Moon <quic_johmoo@quicinc.com> wrote:
>
> One part of maintaining backwards compatibility with older
> userspace programs is avoiding changes to module parameters.
>
> To that end, add a script (check-module-params.sh) which
> performs a simple check of module parameter changes across
> git references.
>
> For example, if this module parameter:
>
> module_param(max_nullfunc_tries, int, 0644);
>
> ...restricted its mode parameter:
>
> module_param(max_nullfunc_tries, int, 0600);
>
> The script would flag the change:
>
> Module parameter "max_nullfunc_tries" in net/mac80211/mlme.c changed!
>   Original args: int,0644
>        New args: int,0600



I know this is just a simple diff, and we cannot expect
accuracy from this tool.



I just tried

  $ ./scripts/check-module-params.sh  -b v6.7-rc1  -p v6.6

Then, I got

  error - 21/576 files with modules parameters appear _not_ to be
backwards compatible



In my understanding, it includes many false alarms.

In most of the cases, the driver was just removed.




[pattern 1] Driver was removed

Module parameter "sal_rec_max" in arch/ia64/kernel/mca_drv.c removed!

 or

Module parameter "up_delay" in
drivers/staging/media/atomisp/i2c/ov5693/atomisp-ov5693.c removed!



[pattern 2] cosmetic change

Module parameter "nested" in arch/x86/kvm/vmx/vmx.c changed!
  Original args: bool,S_IRUGO
       New args: bool,0444





But, it sometimes catches real ones:


Module parameter "ublks_max" in drivers/block/ublk_drv.c changed!
  Original args: int,0444
       New args: &ublk_max_ublks_ops,&ublks_max,0644

 -->  Need a close check



Module parameter "vm_debug" in drivers/gpu/drm/amd/amdgpu/amdgpu_drv.c removed!
  Original args: amdgpu_vm_debug,int,0644

 --> 887db1e49a73 is a breakage, but it seems to be intentional
     according to the commit log








> Signed-off-by: John Moon <quic_johmoo@quicinc.com>
> ---
>  scripts/check-module-params.sh | 295 +++++++++++++++++++++++++++++++++
>  1 file changed, 295 insertions(+)
>  create mode 100755 scripts/check-module-params.sh
>
> diff --git a/scripts/check-module-params.sh b/scripts/check-module-params.sh
> new file mode 100755
> index 000000000000..4d2b2cd483e8
> --- /dev/null
> +++ b/scripts/check-module-params.sh
> @@ -0,0 +1,295 @@
> +#!/bin/bash
> +# SPDX-License-Identifier: GPL-2.0-only
> +# Script to check commits for UAPI backwards compatibility
> +
> +set -o errexit
> +set -o pipefail
> +
> +print_usage() {
> +       name=$(basename "$0")
> +       cat << EOF
> +$name - check for module parameters stability across git commits.
> +
> +By default, the script will check to make sure the latest commit (or current
> +dirty changes) did not introduce changes when compared to HEAD^1. You can
> +check against additional commit ranges with the -b and -p options.
> +
> +Usage: $name [-b BASE_REF] [-p PAST_REF] [-j N] [-l ERROR_LOG] [-q] [-v]
> +
> +Options:
> +    -b BASE_REF    Base git reference to use for comparison. If unspecified or empty,
> +                   will use any dirty changes in tree to UAPI files. If there are no
> +                   dirty changes, HEAD will be used.
> +    -p PAST_REF    Compare BASE_REF to PAST_REF (e.g. -p v6.1). If unspecified or empty,
> +                   will use BASE_REF^1. Must be an ancestor of BASE_REF. Only headers
> +                   that exist on PAST_REF will be checked for compatibility.
> +    -j JOBS        Number of checks to run in parallel (default: number of CPU cores).
> +    -l ERROR_LOG   Write error log to file (default: no error log is generated).
> +    -q             Quiet operation (suppress stdout, still print stderr).
> +    -v             Verbose operation (print more information about each header being checked).
> +
> +Exit codes:
> +    $SUCCESS) Success
> +    $FAILURE) Module param differences detected
> +EOF
> +}
> +
> +readonly SUCCESS=0
> +readonly FAILURE=1
> +
> +# Print to stderr
> +eprintf() {
> +       # shellcheck disable=SC2059
> +       printf "$@" >&2
> +}
> +
> +# Check if git tree is dirty
> +tree_is_dirty() {
> +       ! git diff --quiet
> +}
> +
> +file_module_params_unmodified() {
> +       local file="$1"
> +       local base_ref="$2"
> +       local past_ref="$3"
> +       local base_params_file="${TMP_DIR}/${file}.base"
> +       local past_params_file="${TMP_DIR}/${file}.past"
> +       local error_log="${TMP_DIR}/${file}.error"
> +
> +       local -r awk_cmd='/^ *module_param.*\(/,/.*\);/'
> +
> +       mkdir -p "$(dirname "$error_log")"
> +       git show "${past_ref}:${file}" 2> /dev/null \
> +               | awk "$awk_cmd" > "$past_params_file" || true
> +
> +       # Ignore files that don't exist at the past ref or don't have module params
> +       if [ ! -s "$past_params_file" ]; then
> +               return 255 # Special return code for "no-op"
> +       fi
> +
> +       if [ -z "$base_ref" ]; then
> +               awk "$awk_cmd" "${KERNEL_SRC}/${file}" \
> +                       > "$base_params_file" 2> /dev/null || true
> +       else
> +               git show "${base_ref}:${file}" 2> /dev/null \
> +                       | awk "$awk_cmd" > "$base_params_file" || true
> +       fi
> +
> +       # Process the param data to come up with an associative array of param names to param data
> +       # For example:
> +       #   module_param_call(foo, set_result, get_result, NULL, 0600);
> +       #
> +       # is processed into:
> +       #   pre_change_params[foo]="set_result,get_result,NULL,0600"
> +       local -A pre_change_params
> +       local param_name
> +       local param_params
> +       while read -r mod_param_args; do
> +               param_name="$(echo "$mod_param_args" | cut -d ',' -f 1)"
> +               param_params="$(echo "$mod_param_args" | cut -d ',' -f 2-)"
> +
> +               pre_change_params[$param_name]=$param_params
> +       done < <(tr -d '\t\n ' < "$past_params_file" | tr ';' '\n' | grep -o '(.*)' | tr -d '()')


Maybe

  grep -o '(.*)' | tr -d '()'

can become a single process,

  sed 's/.*(\(.*\))/\1/'





> +
> +       local -A post_change_params
> +       while read -r mod_param_args; do
> +               param_name="$(echo "$mod_param_args" | cut -d ',' -f 1)"
> +               param_params="$(echo "$mod_param_args" | cut -d ',' -f 2-)"
> +
> +               post_change_params[$param_name]=$param_params
> +       done < <(tr -d '\t\n ' < "$base_params_file" | tr ';' '\n' | grep -o '(.*)' | tr -d '()')
> +
> +       # Flag any module param changes that:
> +       #  - Remove/rename a parameter
> +       #  - Change the arguments of the parameter
> +       local incompat_param_changes=0
> +       local pre
> +       local post
> +       for param_name in "${!pre_change_params[@]}"; do
> +               pre="${pre_change_params[$param_name]}"
> +               if [ ! "${post_change_params[$param_name]+set}" ]; then
> +                       {
> +                               printf "Module parameter \"%s\" in %s removed!\n" "$param_name" "$file"
> +                               printf "  Original args: %s\n" "$pre"
> +                       } > "$error_log"
> +                       incompat_param_changes=$((incompat_param_changes + 1))
> +                       continue
> +               fi
> +
> +               post="${post_change_params[$param_name]}"
> +               if [ "$pre" != "$post" ]; then
> +                       {
> +                               printf "Module parameter \"%s\" in %s changed!\n" "$param_name" "$file"
> +                               printf "  Original args: %s\n" "$pre"
> +                               printf "       New args: %s\n" "$post"
> +                       } > "$error_log"
> +                       incompat_param_changes=$((incompat_param_changes + 1))
> +                       continue
> +               fi
> +       done
> +
> +       if [ "$incompat_param_changes" -gt 0 ]; then
> +               return 1
> +       fi
> +}
> +
> +run() {
> +       local base_ref="$1"
> +       local past_ref="$2"
> +       local abi_error_log="$3"
> +
> +       diff_args=("$past_ref")
> +       if [ -n "$base_ref" ]; then
> +               diff_args+=("$base_ref")
> +       fi
> +
> +       local -a threads=()
> +       local passed=0
> +       local failed=0
> +       printf "Checking files between %s and %s for module parameter compatibility...\n" \
> +               "$past_ref" "$base_ref"
> +       while read -r modified_file; do
> +               if [ "${#threads[@]}" -ge "$MAX_THREADS" ]; then
> +                       wait "${threads[0]}" && ret="$?" || ret="$?"
> +                       if [ "$ret" -eq 0 ]; then
> +                               passed=$((passed + 1))
> +                       elif [ "$ret" -eq 1 ]; then
> +                               failed=$((failed + 1))
> +                       fi
> +                       threads=("${threads[@]:1}")
> +               fi
> +
> +               file_module_params_unmodified "$modified_file" "$base_ref" "$past_ref" &
> +               threads+=("$!")
> +       done < <(git diff --diff-filter=MCD --name-only "${diff_args[@]}" -- '*.c' '*.h')
> +
> +       for t in "${threads[@]}"; do
> +               wait "$t" && ret="$?" || ret="$?"
> +               if [ "$ret" -eq 0 ]; then
> +                       passed=$((passed + 1))
> +               elif [ "$ret" -eq 1 ]; then
> +                       failed=$((failed + 1))
> +               fi
> +       done
> +
> +       total=$((passed + failed))
> +       if [ "$total" -eq 0 ]; then
> +               printf "No files with module parameters modified between %s and %s\n" \
> +                       "$past_ref" "${base_ref:-dirty tree}"
> +               exit 0
> +       fi
> +
> +       if [ -n "$abi_error_log" ]; then
> +               printf 'Generated by "%s %s" from git ref %s\n\n' \
> +                       "$0" "$*" "$(git rev-parse HEAD)" > "$abi_error_log"
> +       fi
> +
> +       while read -r error_file; do
> +               {
> +                       cat "$error_file"
> +                       printf "\n\n"
> +               } | tee -a "${abi_error_log:-/dev/null}" >&2
> +       done < <(find "$TMP_DIR" -type f -name '*.error')
> +
> +       if [ "$failed" -gt 0 ]; then
> +               eprintf "error - %d/%d files with modules parameters appear _not_ to be backwards compatible\n" \
> +                       "$failed" "$total"
> +               if [ -n "$abi_error_log" ]; then
> +                       eprintf "Failure summary saved to %s\n" "$abi_error_log"
> +               fi
> +       else
> +               printf "All %d files with module_parameters checked appear to be backwards compatible\n" \
> +                       "$total" "$ARCH"
> +       fi
> +
> +       exit "$failed"
> +}
> +
> +# Make sure the git refs we have make sense
> +check_refs() {
> +       if ! git rev-parse --is-inside-work-tree > /dev/null 2>&1; then
> +               eprintf "error - this script requires the kernel tree to be initialized with Git\n"
> +               return 1
> +       fi
> +
> +       if ! git rev-parse --verify "$past_ref" > /dev/null 2>&1; then
> +               printf 'error - invalid git reference "%s"\n' "$past_ref"
> +               return 1
> +       fi
> +
> +       if [ -n "$base_ref" ]; then
> +               if ! git merge-base --is-ancestor "$past_ref" "$base_ref" > /dev/null 2>&1; then
> +                       printf 'error - "%s" is not an ancestor of base ref "%s"\n' "$past_ref" "$base_ref"
> +                       return 1
> +               fi
> +               if [ "$(git rev-parse "$base_ref")" = "$(git rev-parse "$past_ref")" ]; then
> +                       printf 'error - "%s" and "%s" are the same reference\n' "$past_ref" "$base_ref"
> +                       return 1
> +               fi
> +       fi
> +}
> +
> +main() {
> +       MAX_THREADS=$(nproc)
> +       quiet="false"
> +       local base_ref=""
> +       while getopts "hb:p:j:l:q" opt; do
> +               case $opt in
> +               h)
> +                       print_usage
> +                       exit "$SUCCESS"
> +                       ;;
> +               b)
> +                       base_ref="$OPTARG"
> +                       ;;
> +               p)
> +                       past_ref="$OPTARG"
> +                       ;;
> +               j)
> +                       MAX_THREADS="$OPTARG"
> +                       ;;
> +               l)
> +                       abi_error_log="$OPTARG"
> +                       ;;
> +               q)
> +                       quiet="true"
> +                       ;;
> +               *)
> +                       exit "$FAIL_PREREQ"
> +               esac
> +       done
> +
> +       if [ "$quiet" = "true" ]; then
> +               exec > /dev/null 2>&1
> +       fi
> +
> +       if [ -z "$KERNEL_SRC" ]; then
> +               KERNEL_SRC="$(realpath "$(dirname "$0")"/..)"
> +       fi
> +
> +       cd "$KERNEL_SRC"
> +
> +       if [ -z "$base_ref" ] && ! tree_is_dirty; then
> +               base_ref=HEAD
> +       fi
> +
> +       if [ -z "$past_ref" ]; then
> +               if [ -n "$base_ref" ]; then
> +                       past_ref="${base_ref}^1"
> +               else
> +                       past_ref=HEAD
> +               fi
> +       fi
> +
> +       if ! check_refs; then
> +               exit "$FAIL_PREREQ"
> +       fi
> +
> +       TMP_DIR=$(mktemp -d)
> +       readonly TMP_DIR
> +       trap 'rm -rf "$TMP_DIR"' EXIT
> +
> +       run "$base_ref" "$past_ref" "$abi_error_log"
> +}
> +
> +main "$@"
> --
> 2.17.1
>


--
Best Regards
Masahiro Yamada

^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: [PATCH v6 1/3] check-uapi: Introduce check-uapi.sh
  2023-11-14 10:10   ` Masahiro Yamada
  2023-11-14 12:24     ` Greg Kroah-Hartman
@ 2023-12-11 22:30     ` John Moon
  1 sibling, 0 replies; 8+ messages in thread
From: John Moon @ 2023-12-11 22:30 UTC (permalink / raw)
  To: Masahiro Yamada, Greg Kroah-Hartman
  Cc: Nathan Chancellor, Nick Desaulniers, Nicolas Schier,
	Jonathan Corbet, linux-kbuild, linux-kernel, linux-arm-kernel,
	linux-arm-msm, kernel, Randy Dunlap, Arnd Bergmann,
	Bjorn Andersson, Todd Kjos, Matthias Maennich, Giuliano Procida,
	kernel-team, libabigail, Dodji Seketeli, Trilok Soni,
	Satya Durga Srinivasu Prabhala, Jordan Crouse

On 11/14/2023 2:10 AM, Masahiro Yamada wrote:
> On Sat, Oct 28, 2023 at 4:31 AM John Moon <quic_johmoo@quicinc.com> wrote:
>>
>> While the kernel community has been good at maintaining backwards
>> compatibility with kernel UAPIs, it would be helpful to have a tool
>> to check if a commit introduces changes that break backwards
>> compatibility.
>>
>> To that end, introduce check-uapi.sh: a simple shell script that
>> checks for changes to UAPI headers using libabigail.
>>
>> libabigail is "a framework which aims at helping developers and
>> software distributors to spot some ABI-related issues like interface
>> incompatibility in ELF shared libraries by performing a static
>> analysis of the ELF binaries at hand."
>>
>> The script uses one of libabigail's tools, "abidiff", to compile the
>> changed header before and after the commit to detect any changes.
>>
>> abidiff "compares the ABI of two shared libraries in ELF format. It
>> emits a meaningful report describing the differences between the two
>> ABIs."
>>
>> The script also includes the ability to check the compatibility of
>> all UAPI headers across commits. This allows developers to inspect
>> the stability of the UAPIs over time.
>>
>> Signed-off-by: John Moon <quic_johmoo@quicinc.com>
>> ---
>>      - Add abidiff suppressions to filter out common things like enum
>>        variants named .*_MAX being changed and expansion into padding
>>        fields.
>>      - Bump minimum abidiff version to 2.4 to accomodate new
>>        suppressions.
>>      - Add option (-i) to suppress ambiguous breaking changes.
>>      - Remove printing of full file diffs when ABI breakage is found
>>        as this was too noisy.
>>      - Wait for all files to be checked before printing results as
>>        printing from parallel threads was garbling output.
>>      - Suppress all output when -q is passed.
>>      - Avoid messing up user's git tree by using "git archive" instead
>>        of checking out references.
> 
> 
> 
> The code looks almost good to me.
> 
> (I left some more comments below, but they are minor).
> 
>

Thanks - ACKing your comments. I'll send a v7 shortly which addresses them.

> 
> Greg,
> Could you check the output from the tool?
> Is it OK with you?
> 
> 
> 
> 
> 
> 
> 
> 
> 
> 
> 
> 
>>
>>   scripts/check-uapi.sh | 585 ++++++++++++++++++++++++++++++++++++++++++
>>   1 file changed, 585 insertions(+)
>>   create mode 100755 scripts/check-uapi.sh
>>
>> diff --git a/scripts/check-uapi.sh b/scripts/check-uapi.sh
>> new file mode 100755
>> index 000000000000..bcb03c8dae60
>> --- /dev/null
>> +++ b/scripts/check-uapi.sh
>> @@ -0,0 +1,585 @@
>> +#!/bin/bash
>> +# SPDX-License-Identifier: GPL-2.0-only
>> +# Script to check commits for UAPI backwards compatibility
>> +
>> +set -o errexit
>> +set -o pipefail
>> +
>> +print_usage() {
>> +       name=$(basename "$0")
>> +       cat << EOF
>> +$name - check for UAPI header stability across Git commits
>> +
>> +By default, the script will check to make sure the latest commit (or current
>> +dirty changes) did not introduce ABI changes when compared to HEAD^1. You can
>> +check against additional commit ranges with the -b and -p options.
>> +
>> +The script will not check UAPI headers for architectures other than the one
>> +defined in ARCH.
>> +
>> +Usage: $name [-b BASE_REF] [-p PAST_REF] [-j N] [-l ERROR_LOG] [-i] [-q] [-v]
>> +
>> +Options:
>> +    -b BASE_REF    Base git reference to use for comparison. If unspecified or empty,
>> +                   will use any dirty changes in tree to UAPI files. If there are no
>> +                   dirty changes, HEAD will be used.
>> +    -p PAST_REF    Compare BASE_REF to PAST_REF (e.g. -p v6.1). If unspecified or empty,
>> +                   will use BASE_REF^1. Must be an ancestor of BASE_REF. Only headers
>> +                   that exist on PAST_REF will be checked for compatibility.
>> +    -j JOBS        Number of checks to run in parallel (default: number of CPU cores).
>> +    -l ERROR_LOG   Write error log to file (default: no error log is generated).
>> +    -i             Ignore ambiguous changes that may or may not break UAPI compatibility.
>> +    -q             Quiet operation.
>> +    -v             Verbose operation (print more information about each header being checked).
>> +
>> +Environmental args:
>> +    ABIDIFF  Custom path to abidiff binary
>> +    CC       C compiler (default is "gcc")
>> +    ARCH     Target architecture of C compiler (default is host arch)
> 
> 
> 
> ARCH is not the arch of C compiler.
> It is the arch for the UAPI check.
> 
> It is better to reword it.
> 
> 
> 
> 
> 
> 
>> +
>> +Exit codes:
>> +    $SUCCESS) Success
>> +    $FAIL_ABI) ABI difference detected
>> +    $FAIL_PREREQ) Prerequisite not met
>> +EOF
>> +}
>> +
>> +readonly SUCCESS=0
>> +readonly FAIL_ABI=1
>> +readonly FAIL_PREREQ=2
>> +
>> +# Print to stderr
>> +eprintf() {
>> +       # shellcheck disable=SC2059
>> +       printf "$@" >&2
>> +}
>> +
>> +# Expand an array with a specific character (similar to Python string.join())
>> +join() {
>> +       local IFS="$1"
>> +       shift
>> +       printf "%s" "$*"
>> +}
>> +
>> +# Create abidiff suppressions
>> +gen_suppressions() {
>> +       # Common enum variant names which we don't want to worry about
>> +       # being shifted when new variants are added.
>> +       local -a enum_regex=(
>> +               ".*_AFTER_LAST$"
>> +               ".*_CNT$"
>> +               ".*_COUNT$"
>> +               ".*_END$"
>> +               ".*_LAST$"
>> +               ".*_MASK$"
>> +               ".*_MAX$"
>> +               ".*_MAX_BIT$"
>> +               ".*_MAX_BPF_ATTACH_TYPE$"
>> +               ".*_MAX_ID$"
>> +               ".*_MAX_SHIFT$"
>> +               ".*_NBITS$"
>> +               ".*_NETDEV_NUMHOOKS$"
>> +               ".*_NFT_META_IIFTYPE$"
>> +               ".*_NL80211_ATTR$"
>> +               ".*_NLDEV_NUM_OPS$"
>> +               ".*_NUM$"
>> +               ".*_NUM_ELEMS$"
>> +               ".*_NUM_IRQS$"
>> +               ".*_SIZE$"
>> +               ".*_TLSMAX$"
>> +               "^MAX_.*"
>> +               "^NUM_.*"
>> +       )
>> +
>> +       # Common padding field names which can be expanded into
>> +       # without worrying about users.
>> +       local -a padding_regex=(
>> +               ".*end$"
>> +               ".*pad$"
>> +               ".*pad[0-9]?$"
>> +               ".*pad_[0-9]?$"
>> +               ".*padding$"
>> +               ".*padding[0-9]?$"
>> +               ".*padding_[0-9]?$"
>> +               ".*res$"
>> +               ".*resv$"
>> +               ".*resv[0-9]?$"
>> +               ".*resv_[0-9]?$"
>> +               ".*reserved$"
>> +               ".*reserved[0-9]?$"
>> +               ".*reserved_[0-9]?$"
>> +               ".*rsvd[0-9]?$"
>> +               ".*unused$"
>> +       )
>> +
>> +       cat << EOF
>> +[suppress_type]
>> +  type_kind = enum
>> +  changed_enumerators_regexp = $(join , "${enum_regex[@]}")
>> +EOF
>> +
>> +       for p in "${padding_regex[@]}"; do
>> +               cat << EOF
>> +[suppress_type]
>> +  type_kind = struct
>> +  has_data_member_inserted_at = offset_of_first_data_member_regexp(${p})
>> +EOF
>> +       done
>> +
>> +if [ "$IGNORE_AMBIGUOUS_CHANGES" = "true" ]; then
>> +       cat << EOF
>> +[suppress_type]
>> +  type_kind = struct
>> +  has_data_member_inserted_at = end
>> +  has_size_change = yes
>> +EOF
>> +fi
>> +}
>> +
>> +# Check if git tree is dirty
>> +tree_is_dirty() {
>> +       ! git diff --quiet
>> +}
>> +
>> +# Get list of files installed in $ref
>> +get_file_list() {
>> +       local -r ref="$1"
>> +       local -r tree="$(get_header_tree "$ref")"
>> +
>> +       # Print all installed headers, filtering out ones that can't be compiled
>> +       find "$tree" -type f -name '*.h' -printf '%P\n' | grep -v -f "$INCOMPAT_LIST"
>> +}
>> +
>> +# Add to the list of incompatible headers
>> +add_to_incompat_list() {
>> +       local -r ref="$1"
>> +
>> +       # Start with the usr/include/Makefile to get a list of the headers
>> +       # that don't compile using this method.
>> +       if [ ! -f usr/include/Makefile ]; then
>> +               eprintf "error - no usr/include/Makefile present at %s\n" "$ref"
>> +               eprintf "Note: usr/include/Makefile was added in the v5.3 kernel release\n"
>> +               exit "$FAIL_PREREQ"
>> +       fi
>> +       {
>> +               # shellcheck disable=SC2016
>> +               printf 'all: ; @echo $(no-header-test)\n'
>> +               cat usr/include/Makefile
>> +       } | SRCARCH="$ARCH" make --always-make -f - | tr " " "\n" \
>> +         | grep -v "asm-generic" >> "$INCOMPAT_LIST"
>> +
>> +       # The makefile also skips all asm-generic files, but prints "asm-generic/%"
>> +       # which won't work for our grep match. Instead, print something grep will match.
>> +       printf "asm-generic/.*\.h\n" >> "$INCOMPAT_LIST"
>> +
>> +       sort -u -o "$INCOMPAT_LIST" "$INCOMPAT_LIST"
>> +       sed -i -e '/^$/d' "$INCOMPAT_LIST"
> 
> 
> 
> Maybe, you can move these two lines to the
> end of install_headers().
> 
> The reason for the duplication is because
> run_make_headers_install() is executed twice.
> 
> You do not need to sort INCOMPAT_LIST
> in the first run of run_make_headers_install().
> 
> 
> 
> 
> 
>> +}
>> +
>> +# Compile the simple test app
>> +do_compile() {
>> +       local -r inc_dir="$1"
>> +       local -r header="$2"
>> +       local -r out="$3"
>> +       printf "int main(void) { return 0; }\n" | \
>> +               "$CC" -c \
>> +                 -o "$out" \
>> +                 -x c \
>> +                 -O0 \
>> +                 -std=c90 \
>> +                 -fno-eliminate-unused-debug-types \
>> +                 -g \
>> +                 "-I${inc_dir}" \
>> +                 -include "$header" \
>> +                 -
>> +}
>> +
>> +# Run make headers_install
>> +run_make_headers_install() {
>> +       local -r install_dir="$1"
> 
> 
> 
> You can call get_header_tree() here,
> and pass "$ref" to this function.
> 
> 
> 
> 
> 
>> +       make -j "$MAX_THREADS" ARCH="$ARCH" INSTALL_HDR_PATH="$install_dir" \
>> +               headers_install > /dev/null
>> +}
>> +
>> +# Install headers for both git refs
>> +install_headers() {
>> +       local -r base_ref="$1"
>> +       local -r past_ref="$2"
>> +
>> +       for ref in "$base_ref" "$past_ref"; do
>> +               printf "Installing user-facing UAPI headers from %s... " "${ref:-dirty tree}"
>> +               if [ -n "$ref" ]; then
>> +                       git archive --format=tar --prefix="${ref}-archive/" "$ref" \
>> +                               | (cd "$TMP_DIR" && tar xf -)
>> +                       (
>> +                               cd "${TMP_DIR}/${ref}-archive"
>> +                               run_make_headers_install "${TMP_DIR}/${ref}/usr"
>> +                               add_to_incompat_list "$ref" "$INCOMPAT_LIST"
>> +                       )
>> +               else
>> +                       run_make_headers_install "${TMP_DIR}/${ref}/usr"
>> +                       add_to_incompat_list "$ref" "$INCOMPAT_LIST"
> 
> 
> 
> If we drop the dirty tree support, we can make the code simpler.
> 
> $ref would never become empty, and we would always be able to run
> "make headers_install" in the /tmp directory.
> 
> ("make headers_install" in the working tree would create intermediate
> build artifacts in usr/include/)
> 
> That is my personal opinion.
> 
> 
> If you insist on the dirty tree support,
> I will leave it to you.
> 
> 

Dirty tree support is something Greg requested on v1, so I figure we 
should keep it.

> 
> 
> 
> 
> 
>> +               fi
>> +               printf "OK\n"
>> +       done
>> +}
>> +
>> +# Print the path to the headers_install tree for a given ref
>> +get_header_tree() {
>> +       local -r ref="$1"
>> +       printf "%s" "${TMP_DIR}/${ref}/usr"
>> +}
>> +
>> +# Check file list for UAPI compatibility
>> +check_uapi_files() {
>> +       local -r base_ref="$1"
>> +       local -r past_ref="$2"
>> +       local -r abi_error_log="$3"
>> +
>> +       local passed=0;
>> +       local failed=0;
>> +       local -a threads=()
>> +       set -o errexit
>> +
>> +       printf "Checking changes to UAPI headers between %s and %s...\n" "$past_ref" "${base_ref:-dirty tree}"
>> +       # Loop over all UAPI headers that were installed by $past_ref (if they only exist on $base_ref,
>> +       # there's no way they're broken and no way to compare anyway)
>> +       while read -r file; do
>> +               if [ "${#threads[@]}" -ge "$MAX_THREADS" ]; then
>> +                       if wait "${threads[0]}"; then
>> +                               passed=$((passed + 1))
>> +                       else
>> +                               failed=$((failed + 1))
>> +                       fi
>> +                       threads=("${threads[@]:1}")
>> +               fi
>> +
>> +               check_individual_file "$base_ref" "$past_ref" "$file" &
>> +               threads+=("$!")
>> +       done < <(get_file_list "$past_ref")
>> +
>> +       for t in "${threads[@]}"; do
>> +               if wait "$t"; then
>> +                       passed=$((passed + 1))
>> +               else
>> +                       failed=$((failed + 1))
>> +               fi
>> +       done
>> +
>> +       if [ -n "$abi_error_log" ]; then
>> +               printf 'Generated by "%s %s" from git ref %s\n\n' \
>> +                       "$0" "$*" "$(git rev-parse HEAD)" > "$abi_error_log"
>> +       fi
>> +
>> +       while read -r error_file; do
>> +               {
>> +                       cat "$error_file"
>> +                       printf "\n\n"
>> +               } | tee -a "${abi_error_log:-/dev/null}" >&2
>> +       done < <(find "$TMP_DIR" -type f -name '*.error')
> 
> 
> 
> Does this produce this deterministic result?
> 
> 
> Maybe, adding '| sort' after the 'find' command
> will produce the sorted report?
> 
> 
> 
> 
> 
> 
> 
> 
> 
> 
> 
> 
> 
>> +
>> +       total="$((passed + failed))"
>> +       if [ "$failed" -gt 0 ]; then
>> +               eprintf "error - %d/%d UAPI headers compatible with %s appear _not_ to be backwards compatible\n" \
>> +                       "$failed" "$total" "$ARCH"
>> +               if [ -n "$abi_error_log" ]; then
>> +                       eprintf "Failure summary saved to %s\n" "$abi_error_log"
>> +               fi
>> +       else
>> +               printf "All %d UAPI headers compatible with %s appear to be backwards compatible\n" \
>> +                       "$total" "$ARCH"
>> +       fi
>> +
>> +       return "$failed"
>> +}
>> +
>> +# Check an individual file for UAPI compatibility
>> +check_individual_file() {
>> +       local -r base_ref="$1"
>> +       local -r past_ref="$2"
>> +       local -r file="$3"
>> +
>> +       local -r base_header="$(get_header_tree "$base_ref")/${file}"
>> +       local -r past_header="$(get_header_tree "$past_ref")/${file}"
>> +
>> +       local base_ref_short="${base_ref:-dirty tree}"
>> +       if [ "${#base_ref_short}" -ge 40 ]; then
>> +               base_ref_short="$(git rev-parse --short "$base_ref_short")"
> 
> 
> 
> I believe this is intended to keep the log short, but
> I think we can simply use the ref name passed by the user.
> If ${ref} is crazily long, it is a user's fault.
> 
> 
> 
> 
> 
> 
> 
>> +       fi
>> +
>> +       local past_ref_short="$past_ref"
>> +       if [ "${#past_ref_short}" -ge 40 ]; then
>> +               past_ref_short="$(git rev-parse --short "$past_ref_short")"
>> +       fi
>> +
>> +       if [ ! -f "$base_header" ]; then
>> +               mkdir -p "$(dirname "$base_header")"
>> +               printf "==== UAPI header %s was removed between %s and %s ====" \
>> +                       "$file" "$past_ref_short" "$base_ref_short" \
>> +                               > "${base_header}.error"
>> +               return 1
>> +       fi
>> +
>> +       compare_abi "$file" "$base_header" "$past_header" "$base_ref" "$past_ref" \
>> +               "$base_ref_short" "$past_ref_short"
>> +}
>> +
>> +# Perform the A/B compilation and compare output ABI
>> +compare_abi() {
>> +       local -r file="$1"
>> +       local -r base_header="$2"
>> +       local -r past_header="$3"
>> +       local -r base_ref="$4"
>> +       local -r past_ref="$5"
>> +       local -r base_ref_short="$6"
>> +       local -r past_ref_short="$7"
>> +       local -r log="${TMP_DIR}/log/${file}.log"
>> +
>> +       mkdir -p "$(dirname "$log")"
>> +
>> +       if ! do_compile "$(get_header_tree "$base_ref")/include" "$base_header" "${base_header}.bin" 2> "$log"; then
>> +               {
>> +                       warn_str=$(printf "==== Could not compile version of UAPI header %s at %s ====\n" \
>> +                               "$file" "$base_ref_short")
>> +                       printf "%s\n" "$warn_str"
>> +                       cat "$log"
>> +                       printf -- "=%.0s" $(seq 0 ${#warn_str})
>> +               } > "${base_header}.error"
>> +               return 1
>> +       fi
>> +
>> +       if ! do_compile "$(get_header_tree "$past_ref")/include" "$past_header" "${past_header}.bin" 2> "$log"; then
>> +               {
>> +                       warn_str=$(printf "==== Could not compile version of UAPI header %s at %s ====\n" \
>> +                               "$file" "$past_ref_short")
>> +                       printf "%s\n" "$warn_str"
>> +                       cat "$log"
>> +                       printf -- "=%.0s" $(seq 0 ${#warn_str})
>> +               } > "${base_header}.error"
> 
> 
> 
> You used  >"${base_header}.error" instead of
>> "${past_header}.error" here.
> 
> I guess it is intentional.
> 
> 
> 
> But, I think you can put *.error files
> under the ${TMP_DIR}/log/ directory if you save the log
> from base_ref and past_ref into the same log file.
> 
> 
> For example,
> 
> 
>     local -r error_log="${TMP_DIR}/log/${file}.error"
> 
> 
> Then,
> 
>      {
>            ...
> 
>      } > "${error_log}"
> 
> 
> 
> 
> 
> 
> 
> 
> 
> 
>> +               return 1
>> +       fi
>> +
>> +       local ret=0
>> +       "$ABIDIFF" --non-reachable-types \
>> +               --suppressions "$SUPPRESSIONS" \
>> +               "${past_header}.bin" "${base_header}.bin" > "$log" || ret="$?"
>> +       if [ "$ret" -eq 0 ]; then
>> +               if [ "$VERBOSE" = "true" ]; then
>> +                       printf "No ABI differences detected in %s from %s -> %s\n" \
>> +                               "$file" "$past_ref_short" "$base_ref_short"
>> +               fi
>> +       else
>> +               # Bits in abidiff's return code can be used to determine the type of error
>> +               if [ $((ret & 0x2)) -gt 0 ]; then
>> +                       eprintf "error - abidiff did not run properly\n"
>> +                       exit 1
>> +               fi
>> +
>> +               if [ "$IGNORE_AMBIGUOUS_CHANGES" = "true" ] && [ "$ret" -eq 4 ]; then
>> +                       return 0
>> +               fi
>> +
>> +               # If the only changes were additions (not modifications to existing APIs), then
>> +               # there's no problem. Ignore these diffs.
>> +               if grep "Unreachable types summary" "$log" | grep -q "0 removed" &&
>> +                  grep "Unreachable types summary" "$log" | grep -q "0 changed"; then
>> +                       return 0
>> +               fi
>> +
>> +               {
>> +                       warn_str=$(printf "==== ABI differences detected in %s from %s -> %s ====" \
>> +                               "$file" "$past_ref_short" "$base_ref_short")
>> +                       printf "%s\n" "$warn_str"
>> +                       sed  -e '/summary:/d' -e '/changed type/d' -e '/^$/d' -e 's/^/  /g' "$log"
>> +                       printf -- "=%.0s" $(seq 0 ${#warn_str})
>> +                       if cmp "$past_header" "$base_header" > /dev/null 2>&1; then
>> +                               printf "\n%s did not change between %s and %s...\n" "$file" "$past_ref" "${base_ref:-dirty tree}"
>> +                               printf "It's possible a change to one of the headers it includes caused this error:\n"
>> +                               grep '^#include' "$base_header"
>> +                               printf "\n"
>> +                       fi
>> +               } > "${base_header}.error"
>> +
>> +               return 1
>> +       fi
>> +}
>> +
>> +# Check that a minimum software version number is satisfied
>> +min_version_is_satisfied() {
>> +       local -r min_version="$1"
>> +       local -r version_installed="$2"
>> +
>> +       printf "%s\n%s\n" "$min_version" "$version_installed" \
>> +               | sort -Vc > /dev/null 2>&1
>> +}
>> +
>> +# Make sure we have the tools we need and the arguments make sense
>> +check_deps() {
>> +       ABIDIFF="${ABIDIFF:-abidiff}"
>> +       CC="${CC:-gcc}"
>> +       ARCH="${ARCH:-$(uname -m)}"
>> +       if [ "$ARCH" = "x86_64" ]; then
>> +               ARCH="x86"
>> +       fi
>> +
>> +       local -r abidiff_min_version="2.4"
>> +       local -r libdw_min_version_if_clang="0.171"
>> +
>> +       if ! command -v "$ABIDIFF" > /dev/null 2>&1; then
>> +               eprintf "error - abidiff not found!\n"
>> +               eprintf "Please install abigail-tools version %s or greater\n" "$abidiff_min_version"
>> +               eprintf "See: https://sourceware.org/libabigail/manual/libabigail-overview.html\n"
>> +               return 1
>> +       fi
>> +
>> +       local -r abidiff_version="$("$ABIDIFF" --version | cut -d ' ' -f 2)"
>> +       if ! min_version_is_satisfied "$abidiff_min_version" "$abidiff_version"; then
>> +               eprintf "error - abidiff version too old: %s\n" "$abidiff_version"
>> +               eprintf "Please install abigail-tools version %s or greater\n" "$abidiff_min_version"
>> +               eprintf "See: https://sourceware.org/libabigail/manual/libabigail-overview.html\n"
>> +               return 1
>> +       fi
>> +
>> +       if ! command -v "$CC" > /dev/null 2>&1; then
>> +               eprintf 'error - %s not found\n' "$CC"
>> +               return 1
>> +       fi
>> +
>> +       if "$CC" --version | grep -q clang; then
>> +               local -r libdw_version="$(ldconfig -v 2>/dev/null | grep -v SKIPPED | grep -m 1 -o 'libdw-[0-9]\+.[0-9]\+' | cut -c 7-)"
>> +               if ! min_version_is_satisfied "$libdw_min_version_if_clang" "$libdw_version"; then
>> +                       eprintf "error - libdw version too old for use with clang: %s\n" "$libdw_version"
>> +                       eprintf "Please install libdw from elfutils version %s or greater\n" "$libdw_min_version_if_clang"
>> +                       eprintf "See: https://sourceware.org/elfutils/\n"
>> +                       return 1
>> +               fi
>> +       fi
>> +
>> +       if [ ! -d "arch/${ARCH}" ]; then
>> +               eprintf 'error - ARCH "%s" is not a subdirectory under arch/\n' "$ARCH"
>> +               eprintf "Please set ARCH to one of:\n%s\n" "$(find arch -maxdepth 1 -mindepth 1 -type d -printf '%f ' | fmt)"
>> +               return 1
>> +       fi
>> +
>> +       if ! git rev-parse --is-inside-work-tree > /dev/null 2>&1; then
>> +               eprintf "error - this script requires the kernel tree to be initialized with Git\n"
>> +               return 1
>> +       fi
>> +
>> +       if ! git rev-parse --verify "$past_ref" > /dev/null 2>&1; then
>> +               printf 'error - invalid git reference "%s"\n' "$past_ref"
>> +               return 1
>> +       fi
>> +
>> +       if [ -n "$base_ref" ]; then
>> +               if ! git merge-base --is-ancestor "$past_ref" "$base_ref" > /dev/null 2>&1; then
>> +                       printf 'error - "%s" is not an ancestor of base ref "%s"\n' "$past_ref" "$base_ref"
>> +                       return 1
>> +               fi
>> +               if [ "$(git rev-parse "$base_ref")" = "$(git rev-parse "$past_ref")" ]; then
>> +                       printf 'error - "%s" and "%s" are the same reference\n' "$past_ref" "$base_ref"
>> +                       return 1
>> +               fi
>> +       fi
>> +}
>> +
>> +run() {
>> +       local base_ref="$1"
>> +       local past_ref="$2"
>> +       local abi_error_log="$3"
>> +       shift 3
>> +
>> +       if [ -z "$KERNEL_SRC" ]; then
>> +               KERNEL_SRC="$(realpath "$(dirname "$0")"/..)"
>> +       fi
>> +
>> +       cd "$KERNEL_SRC"
>> +
>> +       if [ -z "$base_ref" ] && ! tree_is_dirty; then
>> +               base_ref=HEAD
>> +       fi
>> +
>> +       if [ -z "$past_ref" ]; then
>> +               if [ -n "$base_ref" ]; then
>> +                       past_ref="${base_ref}^1"
>> +               else
>> +                       past_ref=HEAD
>> +               fi
>> +       fi
>> +
>> +       if ! check_deps; then
>> +               exit "$FAIL_PREREQ"
>> +       fi
>> +
>> +       TMP_DIR=$(mktemp -d)
>> +       readonly TMP_DIR
>> +       trap 'rm -rf "$TMP_DIR"' EXIT
>> +
>> +       readonly INCOMPAT_LIST="${TMP_DIR}/incompat_list.txt"
>> +       touch "$INCOMPAT_LIST"
>> +
>> +       readonly SUPPRESSIONS="${TMP_DIR}/suppressions.txt"
>> +       gen_suppressions > "$SUPPRESSIONS"
>> +
>> +       # Run make install_headers for both refs
>> +       install_headers "$base_ref" "$past_ref"
>> +
>> +       # Check for any differences in the installed header trees
>> +       if diff -r -q "$(get_header_tree "$base_ref")" "$(get_header_tree "$past_ref")" > /dev/null 2>&1; then
>> +               printf "No changes to UAPI headers were applied between %s and %s\n" "$past_ref" "${base_ref:-dirty tree}"
>> +               exit "$SUCCESS"
>> +       fi
>> +
>> +       if ! check_uapi_files "$base_ref" "$past_ref" "$abi_error_log"; then
>> +               exit "$FAIL_ABI"
>> +       fi
>> +}
>> +
>> +main() {
>> +       MAX_THREADS=$(nproc)
>> +       VERBOSE="false"
>> +       IGNORE_AMBIGUOUS_CHANGES="false"
>> +       quiet="false"
>> +       local base_ref=""
>> +       while getopts "hb:p:j:l:iqv" opt; do
>> +               case $opt in
>> +               h)
>> +                       print_usage
>> +                       exit "$SUCCESS"
>> +                       ;;
>> +               b)
>> +                       base_ref="$OPTARG"
>> +                       ;;
>> +               p)
>> +                       past_ref="$OPTARG"
>> +                       ;;
>> +               j)
>> +                       MAX_THREADS="$OPTARG"
>> +                       ;;
>> +               l)
>> +                       abi_error_log="$OPTARG"
>> +                       ;;
>> +               i)
>> +                       IGNORE_AMBIGUOUS_CHANGES="true"
>> +                       ;;
>> +               q)
>> +                       quiet="true"
>> +                       VERBOSE="false"
>> +                       ;;
>> +               v)
>> +                       VERBOSE="true"
>> +                       quiet="false"
>> +                       ;;
>> +               *)
>> +                       exit "$FAIL_PREREQ"
>> +               esac
>> +       done
>> +
>> +       if [ "$quiet" = "true" ]; then
>> +               exec > /dev/null 2>&1
>> +       fi
>> +
>> +       run "$base_ref" "$past_ref" "$abi_error_log" "$@"
>> +}
>> +
>> +main "$@"
>> --
>> 2.17.1
>>
> 
> 
> --
> Best Regards
> 
> Masahiro Yamada

^ permalink raw reply	[flat|nested] 8+ messages in thread

end of thread, other threads:[~2023-12-11 22:30 UTC | newest]

Thread overview: 8+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2023-10-27 19:30 [PATCH v6 0/3] Validating UAPI backwards compatibility John Moon
2023-10-27 19:30 ` [PATCH v6 1/3] check-uapi: Introduce check-uapi.sh John Moon
2023-11-14 10:10   ` Masahiro Yamada
2023-11-14 12:24     ` Greg Kroah-Hartman
2023-12-11 22:30     ` John Moon
2023-10-27 19:30 ` [PATCH v6 2/3] docs: dev-tools: Add UAPI checker documentation John Moon
2023-10-27 19:30 ` [PATCH v6 3/3] check-module-params: Introduce check-module-params.sh John Moon
2023-11-14 13:28   ` Masahiro Yamada

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).