[PATCH v2 1/2] check-uapi: Introduce check-uapi.sh

Masahiro Yamada masahiroy at kernel.org
Sat Mar 4 20:22:58 PST 2023


On Wed, Mar 1, 2023 at 4:54 PM John Moon <quic_johmoo at 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 compatibilty of
> all UAPI headers across commits. This allows developers to inspect
> the stability of the UAPIs over time.


Let's see more test cases.


[Case 1]

I think d759be8953febb6e5b5376c7d9bbf568864c6e2d
is a trivial/good cleanup.
Apparently, it still exports equivalent headers,
but this tool reports "incorrectly removed".



$ ./scripts/check-uapi.sh -b d759be8953
Saving current tree state... OK
Installing sanitized UAPI headers from d759be8953... OK
Installing sanitized UAPI headers from d759be8953^1... OK
Restoring current tree state... OK
Checking changes to UAPI headers starting from d759be8953
error - UAPI header arch/alpha/include/uapi/asm/poll.h was incorrectly removed
error - UAPI header arch/ia64/include/uapi/asm/poll.h was incorrectly removed
error - UAPI header arch/x86/include/uapi/asm/poll.h was incorrectly removed
/tmp/tmp.ixUIBlntUP/d759be8953/x86/usr/include/asm/Kbuild does not
exist - cannot compare ABI
/tmp/tmp.ixUIBlntUP/d759be8953/alpha/usr/include/asm/Kbuild does not
exist - cannot compare ABI
/tmp/tmp.ixUIBlntUP/d759be8953/ia64/usr/include/asm/Kbuild does not
exist - cannot compare ABI
error - 6/6 UAPI headers modified between d759be8953^1 and d759be8953
are not backwards compatible
error - UAPI header ABI check failed
Failure summary saved to /home/masahiro/ref/linux/abi_error_log.txt



[Case 2]

This tool compiles only changed headers.
Does it detect ABI change?

I believe the users of the headers must be compiled.



Think about this case.


$ cat foo-typedef.h
typedef int foo_cap_type;


$ cat foo.h
#include "foo-typedef.h"

struct foo {
       foo_cap_type capability;
};



Then, change the first header to
  typedef long long foo_cap_type;

abidiff will never notice the ABI change
until it compiles "foo.h" instead of "foo-typedef.h"



For testing, I applied the following patch.


 --- 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 */




I believe this is an ABI change because this will change
'struct epoll_event' in the include/uapi/linux/eventpoll.h
but the tool happily reports it is backwards compatible.


$ ./scripts/check-uapi.sh
Saving current tree state... OK
Installing sanitized UAPI headers from HEAD... OK
Installing sanitized UAPI headers from HEAD^1... OK
Restoring current tree state... OK
Checking changes to UAPI headers starting from HEAD
No ABI differences detected in include/uapi/linux/types.h from HEAD^1 -> HEAD
All 1 UAPI headers modified between HEAD^1 and HEAD are backwards compatible!





I would not use such a tool that contains both false positives
and false negatives, but you may notice this is more difficult
than you had expected.

I do not know if further review is worthwhile since this does not work
but I added some more in-line comments.






> +
> +# Some UAPI headers require an architecture-specific compiler to build properly.
> +ARCH_SPECIFIC_CC_NEEDED=(
> +       "arch/hexagon/include/uapi/asm/sigcontext.h"
> +       "arch/ia64/include/uapi/asm/intel_intrin.h"
> +       "arch/ia64/include/uapi/asm/setup.h"
> +       "arch/ia64/include/uapi/asm/sigcontext.h"
> +       "arch/mips/include/uapi/asm/bitfield.h"
> +       "arch/mips/include/uapi/asm/byteorder.h"
> +       "arch/mips/include/uapi/asm/inst.h"
> +       "arch/sparc/include/uapi/asm/fbio.h"
> +       "arch/sparc/include/uapi/asm/uctx.h"
> +       "arch/xtensa/include/uapi/asm/byteorder.h"
> +       "arch/xtensa/include/uapi/asm/msgbuf.h"
> +       "arch/xtensa/include/uapi/asm/sembuf.h"
> +)


Yes, arch/*/include/ must be compiled by the target compiler.
If you compile them by the host compiler, it is unpredictable (i.e. wrong).

BTW, was this blacklist detected on a x86 host?

If you do this on an ARM/ARM64 host, some headers
under arch/x86/include/uapi/ might be blacklisted?



> +# 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:-gcc}" -c \
> +                 -o "$out" \
> +                 -x c \
> +                 -O0 \
> +                 -std=c90 \
> +                 -fno-eliminate-unused-debug-types \
> +                 -g \
> +                 "-I${inc_dir}" \
> +                 -include "$header" \
> +                 -
> +}
> +
> +# Print the list of incompatible headers from the usr/include Makefile
> +get_no_header_list() {
> +       {
> +               # shellcheck disable=SC2016
> +               printf 'all: ; @echo $(no-header-test)\n'
> +               cat "usr/include/Makefile"

You must pass SRCARCH=$arch.

Otherwise,

ifeq ($(SRCARCH),...)
  ...
endif

are all skipped.





> +       } | make -f - | tr " " "\n" | grep -v "asm-generic"
> +
> +       # One additional header file is not building correctly
> +       # with this method.
> +       # TODO: why can't we build this one?
> +       printf "asm-generic/ucontext.h\n"


Answer - it is not intended for standalone compiling in the first place.

<asm-generic/*.h> should be included from <asm/*.h>.

Userspace never ever includes <asm-generic/*.h> directly.
(If it does, it is a bug in the userspace program)

I am afraid you read user/include/Makefile wrongly.




> +
> +# Install headers for every arch and ref we need
> +install_headers() {
> +       local -r check_all="$1"
> +       local -r base_ref="$2"
> +       local -r ref="$3"
> +
> +       local arch_list=()
> +       while read -r status file; do
> +               if arch="$(printf "%s" "$file" | grep -o 'arch/.*/uapi' | cut -d '/' -f 2)"; then
> +                       # shellcheck disable=SC2076
> +                       if ! [[ " ${arch_list[*]} " =~ " $arch " ]]; then
> +                               arch_list+=("$arch")
> +                       fi
> +               fi
> +       done < <(get_uapi_files "$check_all" "$base_ref" "$ref")
> +
> +       deviated_from_current_tree="false"
> +       for inst_ref in "$base_ref" "$ref"; do
> +               if [ -n "$inst_ref" ]; then
> +                       if [ "$deviated_from_current_tree" = "false" ]; then
> +                               save_tree_state
> +                               trap 'rm -rf "$tmp_dir"; restore_tree_state;' EXIT
> +                               deviated_from_current_tree="true"
> +                       fi
> +                       git checkout --quiet "$(git rev-parse "$inst_ref")"


I might be wrong, but I was worried when I looked at this line
because git-checkout may change the running code
if check-uapi.sh is changed between ref and base_ref.

If bash always loads all code into memory before running
it is safe but I do not know how it works.


If this is safe, some comments might be worthwhile:

    # 'git checkout' may update this script itself while running,
    # but it is OK because ...





> +
> +# Make sure we have the tools we need
> +check_deps() {
> +       export ABIDIFF="${ABIDIFF:-abidiff}"
> +
> +       if ! command -v "$ABIDIFF" > /dev/null 2>&1; then
> +               eprintf "error - abidiff not found!\n"
> +               eprintf "Please install abigail-tools (version 1.7 or greater)\n"
> +               eprintf "See: https://sourceware.org/libabigail/manual/libabigail-overview.html\n"
> +               exit 1
> +       fi
> +
> +       read -r abidiff_maj abidiff_min _unused < <("$ABIDIFF" --version | cut -d ' ' -f 2 | tr '.' ' ')
> +       if [ "$abidiff_maj" -lt 1 ] || { [ "$abidiff_maj" -eq 1 ] && [ "$abidiff_min" -lt 7 ]; }; then


This is up to you, but I think "sort -V" would be cleaner.
(see Documentation/devicetree/bindings/Makefile for example)




> +       fi
> +
> +       if [ ! -x "scripts/unifdef" ]; then
> +               if ! make -f /dev/null scripts/unifdef; then

Previously, I wanted to point out that using Make is meaningless,
and using gcc directly is better.


But, is this still necessary?

V2 uses 'make headers_install' to install all headers.
scripts/unifdef is not used anywhere in this script.






> +
> +       abi_error_log="${abi_error_log:-${KERNEL_SRC}/abi_error_log.txt}"
> +
> +       check_deps
> +
> +       tmp_dir=$(mktemp -d)
> +       trap 'rm -rf "$tmp_dir"' EXIT
> +
> +       # Set of UAPI directories to check by default
> +       UAPI_DIRS=(include/uapi arch/*/include/uapi)
> +
> +       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"
> +               exit 1
> +       fi
> +
> +       # If there are no dirty UAPI files, use HEAD as base_ref
> +       if [ -z "$base_ref" ] && [ "$(get_uapi_files "" "" | wc -l)" -eq 0 ]; then
> +               base_ref="HEAD"
> +       fi
> +
> +       if [ -z "$ref_to_check" ]; then
> +               if [ -n "$base_ref" ]; then
> +                       ref_to_check="${base_ref}^1"
> +               else
> +                       ref_to_check="HEAD"
> +               fi
> +       fi


I think this is because I am not good at English, but
I was so confused between 'base_ref' vs 'ref_to_check'.
I do not get which one is the ancestor from the names.

I thought 'ref_a' and 'ref_b' would be less confusing,
but I hope somebody will come up with better naming
than that.




--
Best Regards





Masahiro Yamada



More information about the linux-arm-kernel mailing list