check-uapi.sh 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573
  1. #!/bin/bash
  2. # SPDX-License-Identifier: GPL-2.0-only
  3. # Script to check commits for UAPI backwards compatibility
  4. set -o errexit
  5. set -o pipefail
  6. print_usage() {
  7. name=$(basename "$0")
  8. cat << EOF
  9. $name - check for UAPI header stability across Git commits
  10. By default, the script will check to make sure the latest commit (or current
  11. dirty changes) did not introduce ABI changes when compared to HEAD^1. You can
  12. check against additional commit ranges with the -b and -p options.
  13. The script will not check UAPI headers for architectures other than the one
  14. defined in ARCH.
  15. Usage: $name [-b BASE_REF] [-p PAST_REF] [-j N] [-l ERROR_LOG] [-i] [-q] [-v]
  16. Options:
  17. -b BASE_REF Base git reference to use for comparison. If unspecified or empty,
  18. will use any dirty changes in tree to UAPI files. If there are no
  19. dirty changes, HEAD will be used.
  20. -p PAST_REF Compare BASE_REF to PAST_REF (e.g. -p v6.1). If unspecified or empty,
  21. will use BASE_REF^1. Must be an ancestor of BASE_REF. Only headers
  22. that exist on PAST_REF will be checked for compatibility.
  23. -j JOBS Number of checks to run in parallel (default: number of CPU cores).
  24. -l ERROR_LOG Write error log to file (default: no error log is generated).
  25. -i Ignore ambiguous changes that may or may not break UAPI compatibility.
  26. -q Quiet operation.
  27. -v Verbose operation (print more information about each header being checked).
  28. Environmental args:
  29. ABIDIFF Custom path to abidiff binary
  30. CC C compiler (default is "gcc")
  31. ARCH Target architecture for the UAPI check (default is host arch)
  32. Exit codes:
  33. $SUCCESS) Success
  34. $FAIL_ABI) ABI difference detected
  35. $FAIL_PREREQ) Prerequisite not met
  36. EOF
  37. }
  38. readonly SUCCESS=0
  39. readonly FAIL_ABI=1
  40. readonly FAIL_PREREQ=2
  41. # Print to stderr
  42. eprintf() {
  43. # shellcheck disable=SC2059
  44. printf "$@" >&2
  45. }
  46. # Expand an array with a specific character (similar to Python string.join())
  47. join() {
  48. local IFS="$1"
  49. shift
  50. printf "%s" "$*"
  51. }
  52. # Create abidiff suppressions
  53. gen_suppressions() {
  54. # Common enum variant names which we don't want to worry about
  55. # being shifted when new variants are added.
  56. local -a enum_regex=(
  57. ".*_AFTER_LAST$"
  58. ".*_CNT$"
  59. ".*_COUNT$"
  60. ".*_END$"
  61. ".*_LAST$"
  62. ".*_MASK$"
  63. ".*_MAX$"
  64. ".*_MAX_BIT$"
  65. ".*_MAX_BPF_ATTACH_TYPE$"
  66. ".*_MAX_ID$"
  67. ".*_MAX_SHIFT$"
  68. ".*_NBITS$"
  69. ".*_NETDEV_NUMHOOKS$"
  70. ".*_NFT_META_IIFTYPE$"
  71. ".*_NL80211_ATTR$"
  72. ".*_NLDEV_NUM_OPS$"
  73. ".*_NUM$"
  74. ".*_NUM_ELEMS$"
  75. ".*_NUM_IRQS$"
  76. ".*_SIZE$"
  77. ".*_TLSMAX$"
  78. "^MAX_.*"
  79. "^NUM_.*"
  80. )
  81. # Common padding field names which can be expanded into
  82. # without worrying about users.
  83. local -a padding_regex=(
  84. ".*end$"
  85. ".*pad$"
  86. ".*pad[0-9]?$"
  87. ".*pad_[0-9]?$"
  88. ".*padding$"
  89. ".*padding[0-9]?$"
  90. ".*padding_[0-9]?$"
  91. ".*res$"
  92. ".*resv$"
  93. ".*resv[0-9]?$"
  94. ".*resv_[0-9]?$"
  95. ".*reserved$"
  96. ".*reserved[0-9]?$"
  97. ".*reserved_[0-9]?$"
  98. ".*rsvd[0-9]?$"
  99. ".*unused$"
  100. )
  101. cat << EOF
  102. [suppress_type]
  103. type_kind = enum
  104. changed_enumerators_regexp = $(join , "${enum_regex[@]}")
  105. EOF
  106. for p in "${padding_regex[@]}"; do
  107. cat << EOF
  108. [suppress_type]
  109. type_kind = struct
  110. has_data_member_inserted_at = offset_of_first_data_member_regexp(${p})
  111. EOF
  112. done
  113. if [ "$IGNORE_AMBIGUOUS_CHANGES" = "true" ]; then
  114. cat << EOF
  115. [suppress_type]
  116. type_kind = struct
  117. has_data_member_inserted_at = end
  118. has_size_change = yes
  119. EOF
  120. fi
  121. }
  122. # Check if git tree is dirty
  123. tree_is_dirty() {
  124. ! git diff --quiet
  125. }
  126. # Get list of files installed in $ref
  127. get_file_list() {
  128. local -r ref="$1"
  129. local -r tree="$(get_header_tree "$ref")"
  130. # Print all installed headers, filtering out ones that can't be compiled
  131. find "$tree" -type f -name '*.h' -printf '%P\n' | grep -v -f "$INCOMPAT_LIST"
  132. }
  133. # Add to the list of incompatible headers
  134. add_to_incompat_list() {
  135. local -r ref="$1"
  136. # Start with the usr/include/Makefile to get a list of the headers
  137. # that don't compile using this method.
  138. if [ ! -f usr/include/Makefile ]; then
  139. eprintf "error - no usr/include/Makefile present at %s\n" "$ref"
  140. eprintf "Note: usr/include/Makefile was added in the v5.3 kernel release\n"
  141. exit "$FAIL_PREREQ"
  142. fi
  143. {
  144. # shellcheck disable=SC2016
  145. printf 'all: ; @echo $(no-header-test)\n'
  146. cat usr/include/Makefile
  147. } | SRCARCH="$ARCH" make --always-make -f - | tr " " "\n" \
  148. | grep -v "asm-generic" >> "$INCOMPAT_LIST"
  149. # The makefile also skips all asm-generic files, but prints "asm-generic/%"
  150. # which won't work for our grep match. Instead, print something grep will match.
  151. printf "asm-generic/.*\.h\n" >> "$INCOMPAT_LIST"
  152. }
  153. # Compile the simple test app
  154. do_compile() {
  155. local -r inc_dir="$1"
  156. local -r header="$2"
  157. local -r out="$3"
  158. printf "int main(void) { return 0; }\n" | \
  159. "$CC" -c \
  160. -o "$out" \
  161. -x c \
  162. -O0 \
  163. -std=c90 \
  164. -fno-eliminate-unused-debug-types \
  165. -g \
  166. "-I${inc_dir}" \
  167. -include "$header" \
  168. -
  169. }
  170. # Run make headers_install
  171. run_make_headers_install() {
  172. local -r ref="$1"
  173. local -r install_dir="$(get_header_tree "$ref")"
  174. make -j "$MAX_THREADS" ARCH="$ARCH" INSTALL_HDR_PATH="$install_dir" \
  175. headers_install > /dev/null
  176. }
  177. # Install headers for both git refs
  178. install_headers() {
  179. local -r base_ref="$1"
  180. local -r past_ref="$2"
  181. for ref in "$base_ref" "$past_ref"; do
  182. printf "Installing user-facing UAPI headers from %s... " "${ref:-dirty tree}"
  183. if [ -n "$ref" ]; then
  184. git archive --format=tar --prefix="${ref}-archive/" "$ref" \
  185. | (cd "$TMP_DIR" && tar xf -)
  186. (
  187. cd "${TMP_DIR}/${ref}-archive"
  188. run_make_headers_install "$ref"
  189. add_to_incompat_list "$ref" "$INCOMPAT_LIST"
  190. )
  191. else
  192. run_make_headers_install "$ref"
  193. add_to_incompat_list "$ref" "$INCOMPAT_LIST"
  194. fi
  195. printf "OK\n"
  196. done
  197. sort -u -o "$INCOMPAT_LIST" "$INCOMPAT_LIST"
  198. sed -i -e '/^$/d' "$INCOMPAT_LIST"
  199. }
  200. # Print the path to the headers_install tree for a given ref
  201. get_header_tree() {
  202. local -r ref="$1"
  203. printf "%s" "${TMP_DIR}/${ref}/usr"
  204. }
  205. # Check file list for UAPI compatibility
  206. check_uapi_files() {
  207. local -r base_ref="$1"
  208. local -r past_ref="$2"
  209. local -r abi_error_log="$3"
  210. local passed=0;
  211. local failed=0;
  212. local -a threads=()
  213. set -o errexit
  214. printf "Checking changes to UAPI headers between %s and %s...\n" "$past_ref" "${base_ref:-dirty tree}"
  215. # Loop over all UAPI headers that were installed by $past_ref (if they only exist on $base_ref,
  216. # there's no way they're broken and no way to compare anyway)
  217. while read -r file; do
  218. if [ "${#threads[@]}" -ge "$MAX_THREADS" ]; then
  219. if wait "${threads[0]}"; then
  220. passed=$((passed + 1))
  221. else
  222. failed=$((failed + 1))
  223. fi
  224. threads=("${threads[@]:1}")
  225. fi
  226. check_individual_file "$base_ref" "$past_ref" "$file" &
  227. threads+=("$!")
  228. done < <(get_file_list "$past_ref")
  229. for t in "${threads[@]}"; do
  230. if wait "$t"; then
  231. passed=$((passed + 1))
  232. else
  233. failed=$((failed + 1))
  234. fi
  235. done
  236. if [ -n "$abi_error_log" ]; then
  237. printf 'Generated by "%s %s" from git ref %s\n\n' \
  238. "$0" "$*" "$(git rev-parse HEAD)" > "$abi_error_log"
  239. fi
  240. while read -r error_file; do
  241. {
  242. cat "$error_file"
  243. printf "\n\n"
  244. } | tee -a "${abi_error_log:-/dev/null}" >&2
  245. done < <(find "$TMP_DIR" -type f -name '*.error' | sort)
  246. total="$((passed + failed))"
  247. if [ "$failed" -gt 0 ]; then
  248. eprintf "error - %d/%d UAPI headers compatible with %s appear _not_ to be backwards compatible\n" \
  249. "$failed" "$total" "$ARCH"
  250. if [ -n "$abi_error_log" ]; then
  251. eprintf "Failure summary saved to %s\n" "$abi_error_log"
  252. fi
  253. else
  254. printf "All %d UAPI headers compatible with %s appear to be backwards compatible\n" \
  255. "$total" "$ARCH"
  256. fi
  257. return "$failed"
  258. }
  259. # Check an individual file for UAPI compatibility
  260. check_individual_file() {
  261. local -r base_ref="$1"
  262. local -r past_ref="$2"
  263. local -r file="$3"
  264. local -r base_header="$(get_header_tree "$base_ref")/${file}"
  265. local -r past_header="$(get_header_tree "$past_ref")/${file}"
  266. if [ ! -f "$base_header" ]; then
  267. mkdir -p "$(dirname "$base_header")"
  268. printf "==== UAPI header %s was removed between %s and %s ====" \
  269. "$file" "$past_ref" "$base_ref" \
  270. > "${base_header}.error"
  271. return 1
  272. fi
  273. compare_abi "$file" "$base_header" "$past_header" "$base_ref" "$past_ref"
  274. }
  275. # Perform the A/B compilation and compare output ABI
  276. compare_abi() {
  277. local -r file="$1"
  278. local -r base_header="$2"
  279. local -r past_header="$3"
  280. local -r base_ref="$4"
  281. local -r past_ref="$5"
  282. local -r log="${TMP_DIR}/log/${file}.log"
  283. local -r error_log="${TMP_DIR}/log/${file}.error"
  284. mkdir -p "$(dirname "$log")"
  285. if ! do_compile "$(get_header_tree "$base_ref")/include" "$base_header" "${base_header}.bin" 2> "$log"; then
  286. {
  287. warn_str=$(printf "==== Could not compile version of UAPI header %s at %s ====\n" \
  288. "$file" "$base_ref")
  289. printf "%s\n" "$warn_str"
  290. cat "$log"
  291. printf -- "=%.0s" $(seq 0 ${#warn_str})
  292. } > "$error_log"
  293. return 1
  294. fi
  295. if ! do_compile "$(get_header_tree "$past_ref")/include" "$past_header" "${past_header}.bin" 2> "$log"; then
  296. {
  297. warn_str=$(printf "==== Could not compile version of UAPI header %s at %s ====\n" \
  298. "$file" "$past_ref")
  299. printf "%s\n" "$warn_str"
  300. cat "$log"
  301. printf -- "=%.0s" $(seq 0 ${#warn_str})
  302. } > "$error_log"
  303. return 1
  304. fi
  305. local ret=0
  306. "$ABIDIFF" --non-reachable-types \
  307. --suppressions "$SUPPRESSIONS" \
  308. "${past_header}.bin" "${base_header}.bin" > "$log" || ret="$?"
  309. if [ "$ret" -eq 0 ]; then
  310. if [ "$VERBOSE" = "true" ]; then
  311. printf "No ABI differences detected in %s from %s -> %s\n" \
  312. "$file" "$past_ref" "$base_ref"
  313. fi
  314. else
  315. # Bits in abidiff's return code can be used to determine the type of error
  316. if [ $((ret & 0x2)) -gt 0 ]; then
  317. eprintf "error - abidiff did not run properly\n"
  318. exit 1
  319. fi
  320. if [ "$IGNORE_AMBIGUOUS_CHANGES" = "true" ] && [ "$ret" -eq 4 ]; then
  321. return 0
  322. fi
  323. # If the only changes were additions (not modifications to existing APIs), then
  324. # there's no problem. Ignore these diffs.
  325. if grep "Unreachable types summary" "$log" | grep -q "0 removed" &&
  326. grep "Unreachable types summary" "$log" | grep -q "0 changed"; then
  327. return 0
  328. fi
  329. {
  330. warn_str=$(printf "==== ABI differences detected in %s from %s -> %s ====" \
  331. "$file" "$past_ref" "$base_ref")
  332. printf "%s\n" "$warn_str"
  333. sed -e '/summary:/d' -e '/changed type/d' -e '/^$/d' -e 's/^/ /g' "$log"
  334. printf -- "=%.0s" $(seq 0 ${#warn_str})
  335. if cmp "$past_header" "$base_header" > /dev/null 2>&1; then
  336. printf "\n%s did not change between %s and %s...\n" "$file" "$past_ref" "${base_ref:-dirty tree}"
  337. printf "It's possible a change to one of the headers it includes caused this error:\n"
  338. grep '^#include' "$base_header"
  339. printf "\n"
  340. fi
  341. } > "$error_log"
  342. return 1
  343. fi
  344. }
  345. # Check that a minimum software version number is satisfied
  346. min_version_is_satisfied() {
  347. local -r min_version="$1"
  348. local -r version_installed="$2"
  349. printf "%s\n%s\n" "$min_version" "$version_installed" \
  350. | sort -Vc > /dev/null 2>&1
  351. }
  352. # Make sure we have the tools we need and the arguments make sense
  353. check_deps() {
  354. ABIDIFF="${ABIDIFF:-abidiff}"
  355. CC="${CC:-gcc}"
  356. ARCH="${ARCH:-$(uname -m)}"
  357. if [ "$ARCH" = "x86_64" ]; then
  358. ARCH="x86"
  359. fi
  360. local -r abidiff_min_version="2.4"
  361. local -r libdw_min_version_if_clang="0.171"
  362. if ! command -v "$ABIDIFF" > /dev/null 2>&1; then
  363. eprintf "error - abidiff not found!\n"
  364. eprintf "Please install abigail-tools version %s or greater\n" "$abidiff_min_version"
  365. eprintf "See: https://sourceware.org/libabigail/manual/libabigail-overview.html\n"
  366. return 1
  367. fi
  368. local -r abidiff_version="$("$ABIDIFF" --version | cut -d ' ' -f 2)"
  369. if ! min_version_is_satisfied "$abidiff_min_version" "$abidiff_version"; then
  370. eprintf "error - abidiff version too old: %s\n" "$abidiff_version"
  371. eprintf "Please install abigail-tools version %s or greater\n" "$abidiff_min_version"
  372. eprintf "See: https://sourceware.org/libabigail/manual/libabigail-overview.html\n"
  373. return 1
  374. fi
  375. if ! command -v "$CC" > /dev/null 2>&1; then
  376. eprintf 'error - %s not found\n' "$CC"
  377. return 1
  378. fi
  379. if "$CC" --version | grep -q clang; then
  380. local -r libdw_version="$(ldconfig -v 2>/dev/null | grep -v SKIPPED | grep -m 1 -o 'libdw-[0-9]\+.[0-9]\+' | cut -c 7-)"
  381. if ! min_version_is_satisfied "$libdw_min_version_if_clang" "$libdw_version"; then
  382. eprintf "error - libdw version too old for use with clang: %s\n" "$libdw_version"
  383. eprintf "Please install libdw from elfutils version %s or greater\n" "$libdw_min_version_if_clang"
  384. eprintf "See: https://sourceware.org/elfutils/\n"
  385. return 1
  386. fi
  387. fi
  388. if [ ! -d "arch/${ARCH}" ]; then
  389. eprintf 'error - ARCH "%s" is not a subdirectory under arch/\n' "$ARCH"
  390. eprintf "Please set ARCH to one of:\n%s\n" "$(find arch -maxdepth 1 -mindepth 1 -type d -printf '%f ' | fmt)"
  391. return 1
  392. fi
  393. if ! git rev-parse --is-inside-work-tree > /dev/null 2>&1; then
  394. eprintf "error - this script requires the kernel tree to be initialized with Git\n"
  395. return 1
  396. fi
  397. if ! git rev-parse --verify "$past_ref" > /dev/null 2>&1; then
  398. printf 'error - invalid git reference "%s"\n' "$past_ref"
  399. return 1
  400. fi
  401. if [ -n "$base_ref" ]; then
  402. if ! git merge-base --is-ancestor "$past_ref" "$base_ref" > /dev/null 2>&1; then
  403. printf 'error - "%s" is not an ancestor of base ref "%s"\n' "$past_ref" "$base_ref"
  404. return 1
  405. fi
  406. if [ "$(git rev-parse "$base_ref")" = "$(git rev-parse "$past_ref")" ]; then
  407. printf 'error - "%s" and "%s" are the same reference\n' "$past_ref" "$base_ref"
  408. return 1
  409. fi
  410. fi
  411. }
  412. run() {
  413. local base_ref="$1"
  414. local past_ref="$2"
  415. local abi_error_log="$3"
  416. shift 3
  417. if [ -z "$KERNEL_SRC" ]; then
  418. KERNEL_SRC="$(realpath "$(dirname "$0")"/..)"
  419. fi
  420. cd "$KERNEL_SRC"
  421. if [ -z "$base_ref" ] && ! tree_is_dirty; then
  422. base_ref=HEAD
  423. fi
  424. if [ -z "$past_ref" ]; then
  425. if [ -n "$base_ref" ]; then
  426. past_ref="${base_ref}^1"
  427. else
  428. past_ref=HEAD
  429. fi
  430. fi
  431. if ! check_deps; then
  432. exit "$FAIL_PREREQ"
  433. fi
  434. TMP_DIR=$(mktemp -d)
  435. readonly TMP_DIR
  436. trap 'rm -rf "$TMP_DIR"' EXIT
  437. readonly INCOMPAT_LIST="${TMP_DIR}/incompat_list.txt"
  438. touch "$INCOMPAT_LIST"
  439. readonly SUPPRESSIONS="${TMP_DIR}/suppressions.txt"
  440. gen_suppressions > "$SUPPRESSIONS"
  441. # Run make install_headers for both refs
  442. install_headers "$base_ref" "$past_ref"
  443. # Check for any differences in the installed header trees
  444. if diff -r -q "$(get_header_tree "$base_ref")" "$(get_header_tree "$past_ref")" > /dev/null 2>&1; then
  445. printf "No changes to UAPI headers were applied between %s and %s\n" "$past_ref" "${base_ref:-dirty tree}"
  446. exit "$SUCCESS"
  447. fi
  448. if ! check_uapi_files "$base_ref" "$past_ref" "$abi_error_log"; then
  449. exit "$FAIL_ABI"
  450. fi
  451. }
  452. main() {
  453. MAX_THREADS=$(nproc)
  454. VERBOSE="false"
  455. IGNORE_AMBIGUOUS_CHANGES="false"
  456. quiet="false"
  457. local base_ref=""
  458. while getopts "hb:p:j:l:iqv" opt; do
  459. case $opt in
  460. h)
  461. print_usage
  462. exit "$SUCCESS"
  463. ;;
  464. b)
  465. base_ref="$OPTARG"
  466. ;;
  467. p)
  468. past_ref="$OPTARG"
  469. ;;
  470. j)
  471. MAX_THREADS="$OPTARG"
  472. ;;
  473. l)
  474. abi_error_log="$OPTARG"
  475. ;;
  476. i)
  477. IGNORE_AMBIGUOUS_CHANGES="true"
  478. ;;
  479. q)
  480. quiet="true"
  481. VERBOSE="false"
  482. ;;
  483. v)
  484. VERBOSE="true"
  485. quiet="false"
  486. ;;
  487. *)
  488. exit "$FAIL_PREREQ"
  489. esac
  490. done
  491. if [ "$quiet" = "true" ]; then
  492. exec > /dev/null 2>&1
  493. fi
  494. run "$base_ref" "$past_ref" "$abi_error_log" "$@"
  495. }
  496. main "$@"