diff mbox series

[RFC,bpf-next,10/12] selftests/bpf: Script to verify uapi headers usage with vmlinux.h

Message ID 20221025222802.2295103-11-eddyz87@gmail.com (mailing list archive)
State RFC
Delegated to: BPF
Headers show
Series Use uapi kernel headers with vmlinux.h | expand

Checks

Context Check Description
bpf/vmtest-bpf-next-PR fail PR summary
bpf/vmtest-bpf-next-VM_Test-1 pending Logs for ${{ matrix.test }} on ${{ matrix.arch }} with ${{ matrix.toolchain }}
bpf/vmtest-bpf-next-VM_Test-2 fail Logs for build for s390x with gcc
bpf/vmtest-bpf-next-VM_Test-3 fail Logs for build for x86_64 with gcc
bpf/vmtest-bpf-next-VM_Test-4 fail Logs for build for x86_64 with llvm-16
bpf/vmtest-bpf-next-VM_Test-5 success Logs for llvm-toolchain
bpf/vmtest-bpf-next-VM_Test-6 success Logs for set-matrix
netdev/tree_selection success Clearly marked for bpf-next, async
netdev/fixes_present success Fixes tag not required for -next series
netdev/subject_prefix success Link
netdev/cover_letter success Series has a cover letter
netdev/patch_count success Link
netdev/header_inline success No static functions without inline keyword in header files
netdev/build_32bit success Errors and warnings before: 0 this patch: 0
netdev/cc_maintainers warning 14 maintainers not CCed: sdf@google.com llvm@lists.linux.dev john.fastabend@gmail.com trix@redhat.com haoluo@google.com linux-kselftest@vger.kernel.org jolsa@kernel.org ndesaulniers@google.com kpsingh@kernel.org song@kernel.org shuah@kernel.org mykolal@fb.com martin.lau@linux.dev nathan@kernel.org
netdev/build_clang success Errors and warnings before: 0 this patch: 0
netdev/module_param success Was 0 now: 0
netdev/verify_signedoff success Signed-off-by tag matches author and committer
netdev/check_selftest success No net selftest shell script
netdev/verify_fixes success No Fixes tag
netdev/build_allmodconfig_warn success Errors and warnings before: 0 this patch: 0
netdev/checkpatch warning WARNING: added, moved or deleted file(s), does MAINTAINERS need updating?
netdev/kdoc success Errors and warnings before: 0 this patch: 0
netdev/source_inline success Was 0 now: 0

Commit Message

Eduard Zingerman Oct. 25, 2022, 10:27 p.m. UTC
A script to test header guards in vmlinux.h by compiling a simple C
snippet for a set of selected UAPI headers. The snippet being
compiled looks as follows:

  #include <some_uapi_header.h>
  #include "vmlinux.h"

  __attribute__((section("tc"), used))
  int syncookie_tc(struct __sk_buff *skb) { return 0; }

If header guards are placed correctly in vmlinux.h the snippet
should compile w/o errors.

The list of known good headers is supposed to be located in
`tools/testing/selftests/bpf/good_uapi_headers.txt` added as a
separate commit.

Signed-off-by: Eduard Zingerman <eddyz87@gmail.com>
---
 .../selftests/bpf/test_uapi_headers.py        | 197 ++++++++++++++++++
 1 file changed, 197 insertions(+)
 create mode 100755 tools/testing/selftests/bpf/test_uapi_headers.py
diff mbox series

Patch

diff --git a/tools/testing/selftests/bpf/test_uapi_headers.py b/tools/testing/selftests/bpf/test_uapi_headers.py
new file mode 100755
index 000000000000..1740c4fe0625
--- /dev/null
+++ b/tools/testing/selftests/bpf/test_uapi_headers.py
@@ -0,0 +1,197 @@ 
+#!/usr/bin/env python3
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+
+# A script to test header guards in vmlinux.h by compiling a simple C
+# snippet for a set of selected UAPI headers. The snippet being
+# compiled looks as follows:
+#
+#   #include <some_uapi_header.h>
+#   #include "vmlinux.h"
+#
+#   __attribute__((section("tc"), used))
+#   int syncookie_tc(struct __sk_buff *skb) { return 0; }
+#
+# If header guards are placed correctly in vmlinux.h the snippet
+# should compile w/o errors.
+#
+# The script could be used in two modes:
+# - interactive BPF testing and CI;
+# - debug mode.
+#
+# * Interactive BPF testing and CI
+#
+# Run script as follows:
+#
+#   ./test_uapi_headers.py
+#
+# In this mode the following actions are performed:
+# - kernel headers are installed to a temporary directory;
+# - a list of known good uapi headers is read from ./good_uapi_headers.txt;
+# - the snippet above is compiled by clang using BPF target for each header;
+# - if shell is interactive the progress / ETA are reported during execution;
+# - pass / fail statistics is reported in the end;
+# - headers temporary directory is deleted;
+# - script exit code is 0 if snippet could be compiled for all headers.
+#
+# The vmlinux.h processing time is significant (~700ms using Intel i7-4710HQ),
+# thus the headers are processed in parallel.
+#
+# * Debug mode
+#
+# The following parameters are available for debugging:
+#
+#   test_uapi_headers.py \
+#            [-h] [--kheaders KHEADERS] [--vmlinuxh VMLINUXH] [--test TEST]
+#
+#   options:
+#     -h, --help           show this help message and exit
+#     --kheaders KHEADERS  path to exported kernel headers
+#     --vmlinuxh VMLINUXH  path to vmlinux.h
+#     --test TEST          name of the header -or-
+#                          file with header names -or-
+#                          special value '*'
+#
+# When --kheaders is specified the temporary directory is not created
+# and KHEADERS is used instead. It is assumed that headers are already
+# installed to KHEADERS.
+#
+# When TEST names a header (e.g. 'linux/tcp.h') it is the to test.
+# When TEST names a file this file should contain a list of
+# headers to test one per line.
+# When TEST is '*' all exported headers are tested.
+#
+# The simplest way to debug an issue with a single header is:
+#
+#   ./test_uapi_headers.py --test linux/tcp.h
+
+import subprocess
+import concurrent.futures
+import pathlib
+import time
+import os
+import sys
+import argparse
+import tempfile
+import shutil
+import atexit
+from dataclasses import dataclass
+
+@dataclass
+class Result:
+    header: pathlib.Path
+    returncode: int
+    stderr: str
+
+def run_one(header, kheaders, vmlinuxh):
+    code=f'''
+#include <{header}>
+#include "{vmlinuxh}"
+
+__attribute__((section("tc"), used))
+int syncookie_tc(struct __sk_buff *skb)
+{{
+    return 0;
+}}
+    '''
+    command = f'''
+{os.getenv('CLANG', 'clang')} \
+    -g -Werror -mlittle-endian \
+    -D__x86_64__ \
+    -Xclang -fwchar-type=short \
+    -Xclang -fno-signed-wchar \
+    -I{kheaders}/include/ \
+    -Wno-compare-distinct-pointer-types \
+    -mcpu=v3 \
+    -O2 \
+    -target bpf \
+    -x c \
+    -o /dev/null \
+    -fsyntax-only \
+    -
+'''
+    proc = subprocess.run(command, input=code, capture_output=True,
+                          shell=True, encoding='utf8')
+    return Result(header=header,
+                  returncode=proc.returncode,
+                  stderr=proc.stderr)
+
+def run_all(headers, kheaders, vmlinuxh):
+    start_time = time.time()
+    ok = 0
+    fail = 0
+    failures = []
+    remain = len(headers)
+    print_progress = sys.stdout.isatty()
+    print(f'Processing {remain} headers.')
+    with concurrent.futures.ThreadPoolExecutor(max_workers=os.cpu_count()) as executor:
+        for result in executor.map(lambda header: run_one(header, kheaders, vmlinuxh),
+                                   headers):
+            if result.returncode == 0:
+                print(f"{result.header:<60}   ok")
+                ok += 1
+            else:
+                print(f"{result.header:<60} fail")
+                fail += 1
+                failures.append(result)
+            remain -= 1
+            if print_progress:
+                elapsed = time.time() - start_time
+                processed = ok + fail
+                time_per_header = elapsed / processed
+                eta = int(remain * time_per_header)
+                # keep this shorter than header ok/fail line
+                line = f"Ok {ok: >4} Fail {fail: >4} Remain {remain: >4} ETA {eta: >4}s"
+                print(line, end="\r")
+    if print_progress:
+        print('')
+    elapsed = int(time.time() - start_time)
+    if fail == 0:
+        print(f"Done in {elapsed}s, all {len(headers)} ok.")
+    else:
+        print('----- Failure details -----')
+        for result in failures:
+            print(f'{result.header}: rc = {result.returncode}')
+            for line in result.stderr.split('\n'):
+                print(f"{result.header}: {line}")
+        print(f"Done in {elapsed}s, {fail} out of {len(headers)} failed.")
+    return fail == 0
+
+def main(argv):
+    bpf_test_dir = pathlib.Path(__file__).resolve().parent
+    default_vmlinuxh = bpf_test_dir / './tools/include/vmlinux.h'
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--kheaders", type=str, help='path to exported kernel headers')
+    parser.add_argument("--vmlinuxh", type=str, default=default_vmlinuxh,
+                        help='path to vmlinux.h')
+    parser.add_argument("--test", type=str,
+                        default='./good_uapi_headers.txt',
+                        help="name of the header | file with header names | special value '*'")
+    args = parser.parse_args(argv)
+
+    if args.kheaders is None:
+        kheaders = tempfile.mkdtemp(prefix='kheaders')
+        atexit.register(lambda: shutil.rmtree(kheaders))
+        kernel_dir = bpf_test_dir / '../../../../'
+        # Capture both stdout and stderr as stdout to simplify CI logging
+        subprocess.run(f'make -C {kernel_dir} INSTALL_HDR_PATH={kheaders} headers_install',
+                       stdout=sys.stdout, stderr=sys.stdout,
+                       check=True, shell=True)
+    else:
+        kheaders = args.kheaders
+
+    if os.path.exists(args.test):
+        with open(args.test, 'r') as list_file:
+            headers = [line.strip() for line in list_file]
+    elif args.test == '*':
+        headers = [p.relative_to(f'{kheaders}/include').as_posix()
+                   for p in pathlib.Path(kheaders).rglob("*.h")]
+    else:
+        headers = [args.test]
+
+    if run_all(headers, kheaders, args.vmlinuxh):
+        sys.exit(0)
+    else:
+        sys.exit(1)
+
+if __name__ == '__main__':
+    main(sys.argv[1:])