Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux

selftests/bpf: Check consistency between bpftool source, doc, completion

Whenever the eBPF subsystem gains new elements, such as new program or
map types, it is necessary to update bpftool if we want it able to
handle the new items.

In addition to the main arrays containing the names of these elements in
the source code, there are also multiple locations to update:

- The help message in the do_help() functions in bpftool's source code.
- The RST documentation files.
- The bash completion file.

This has led to omissions multiple times in the past. This patch
attempts to address this issue by adding consistency checks for all
these different locations. It also verifies that the bpf_prog_type,
bpf_map_type and bpf_attach_type enums from the UAPI BPF header have all
their members present in bpftool.

The script requires no argument to run, it reads and parses the
different files to check, and prints the mismatches, if any. It
currently reports a number of missing elements, which will be fixed in a
later patch:

$ ./test_bpftool_synctypes.py
Comparing [...]/linux/tools/bpf/bpftool/map.c (map_type_name) and [...]/linux/tools/bpf/bpftool/bash-completion/bpftool (BPFTOOL_MAP_CREATE_TYPES): {'ringbuf'}
Comparing BPF header (enum bpf_attach_type) and [...]/linux/tools/bpf/bpftool/common.c (attach_type_name): {'BPF_TRACE_ITER', 'BPF_XDP_DEVMAP', 'BPF_XDP', 'BPF_SK_REUSEPORT_SELECT', 'BPF_XDP_CPUMAP', 'BPF_SK_REUSEPORT_SELECT_OR_MIGRATE'}
Comparing [...]/linux/tools/bpf/bpftool/prog.c (attach_type_strings) and [...]/linux/tools/bpf/bpftool/prog.c (do_help() ATTACH_TYPE): {'skb_verdict'}
Comparing [...]/linux/tools/bpf/bpftool/prog.c (attach_type_strings) and [...]/linux/tools/bpf/bpftool/Documentation/bpftool-prog.rst (ATTACH_TYPE): {'skb_verdict'}
Comparing [...]/linux/tools/bpf/bpftool/prog.c (attach_type_strings) and [...]/linux/tools/bpf/bpftool/bash-completion/bpftool (BPFTOOL_PROG_ATTACH_TYPES): {'skb_verdict'}

Note that the script does NOT check for consistency between the list of
program types that bpftool claims it accepts and the actual list of
keywords that can be used. This is because bpftool does not "see" them,
they are ELF section names parsed by libbpf. It is not hard to parse the
section_defs[] array in libbpf, but some section names are associated
with program types that bpftool cannot load at the moment. For example,
some programs require a BTF target and an attach target that bpftool
cannot handle. The script may be extended to parse the array and check
only relevant values in the future.

The script is not added to the selftests' Makefile, because doing so
would require all patches with BPF UAPI change to also update bpftool.
Instead it is to be added to the CI.

Signed-off-by: Quentin Monnet <quentin@isovalent.com>
Signed-off-by: Andrii Nakryiko <andrii@kernel.org>
Link: https://lore.kernel.org/bpf/20210730215435.7095-3-quentin@isovalent.com

authored by

Quentin Monnet and committed by
Andrii Nakryiko
a2b5944f 510b4d4c

+486
+486
tools/testing/selftests/bpf/test_bpftool_synctypes.py
··· 1 + #!/usr/bin/env python3 2 + # SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) 3 + # 4 + # Copyright (C) 2021 Isovalent, Inc. 5 + 6 + import argparse 7 + import re 8 + import os, sys 9 + 10 + LINUX_ROOT = os.path.abspath(os.path.join(__file__, 11 + os.pardir, os.pardir, os.pardir, os.pardir, os.pardir)) 12 + BPFTOOL_DIR = os.path.join(LINUX_ROOT, 'tools/bpf/bpftool') 13 + retval = 0 14 + 15 + class BlockParser(object): 16 + """ 17 + A parser for extracting set of values from blocks such as enums. 18 + @reader: a pointer to the open file to parse 19 + """ 20 + def __init__(self, reader): 21 + self.reader = reader 22 + 23 + def search_block(self, start_marker): 24 + """ 25 + Search for a given structure in a file. 26 + @start_marker: regex marking the beginning of a structure to parse 27 + """ 28 + offset = self.reader.tell() 29 + array_start = re.search(start_marker, self.reader.read()) 30 + if array_start is None: 31 + raise Exception('Failed to find start of block') 32 + self.reader.seek(offset + array_start.start()) 33 + 34 + def parse(self, pattern, end_marker): 35 + """ 36 + Parse a block and return a set of values. Values to extract must be 37 + on separate lines in the file. 38 + @pattern: pattern used to identify the values to extract 39 + @end_marker: regex marking the end of the block to parse 40 + """ 41 + entries = set() 42 + while True: 43 + line = self.reader.readline() 44 + if not line or re.match(end_marker, line): 45 + break 46 + capture = pattern.search(line) 47 + if capture and pattern.groups >= 1: 48 + entries.add(capture.group(1)) 49 + return entries 50 + 51 + class ArrayParser(BlockParser): 52 + """ 53 + A parser for extracting dicionaries of values from some BPF-related arrays. 54 + @reader: a pointer to the open file to parse 55 + @array_name: name of the array to parse 56 + """ 57 + end_marker = re.compile('^};') 58 + 59 + def __init__(self, reader, array_name): 60 + self.array_name = array_name 61 + self.start_marker = re.compile(f'(static )?const char \* const {self.array_name}\[.*\] = {{\n') 62 + super().__init__(reader) 63 + 64 + def search_block(self): 65 + """ 66 + Search for the given array in a file. 67 + """ 68 + super().search_block(self.start_marker); 69 + 70 + def parse(self): 71 + """ 72 + Parse a block and return data as a dictionary. Items to extract must be 73 + on separate lines in the file. 74 + """ 75 + pattern = re.compile('\[(BPF_\w*)\]\s*= "(.*)",?$') 76 + entries = {} 77 + while True: 78 + line = self.reader.readline() 79 + if line == '' or re.match(self.end_marker, line): 80 + break 81 + capture = pattern.search(line) 82 + if capture: 83 + entries[capture.group(1)] = capture.group(2) 84 + return entries 85 + 86 + class InlineListParser(BlockParser): 87 + """ 88 + A parser for extracting set of values from inline lists. 89 + """ 90 + def parse(self, pattern, end_marker): 91 + """ 92 + Parse a block and return a set of values. Multiple values to extract 93 + can be on a same line in the file. 94 + @pattern: pattern used to identify the values to extract 95 + @end_marker: regex marking the end of the block to parse 96 + """ 97 + entries = set() 98 + while True: 99 + line = self.reader.readline() 100 + if not line: 101 + break 102 + entries.update(pattern.findall(line)) 103 + if re.search(end_marker, line): 104 + break 105 + return entries 106 + 107 + class FileExtractor(object): 108 + """ 109 + A generic reader for extracting data from a given file. This class contains 110 + several helper methods that wrap arround parser objects to extract values 111 + from different structures. 112 + This class does not offer a way to set a filename, which is expected to be 113 + defined in children classes. 114 + """ 115 + def __init__(self): 116 + self.reader = open(self.filename, 'r') 117 + 118 + def close(self): 119 + """ 120 + Close the file used by the parser. 121 + """ 122 + self.reader.close() 123 + 124 + def reset_read(self): 125 + """ 126 + Reset the file position indicator for this parser. This is useful when 127 + parsing several structures in the file without respecting the order in 128 + which those structures appear in the file. 129 + """ 130 + self.reader.seek(0) 131 + 132 + def get_types_from_array(self, array_name): 133 + """ 134 + Search for and parse an array associating names to BPF_* enum members, 135 + for example: 136 + 137 + const char * const prog_type_name[] = { 138 + [BPF_PROG_TYPE_UNSPEC] = "unspec", 139 + [BPF_PROG_TYPE_SOCKET_FILTER] = "socket_filter", 140 + [BPF_PROG_TYPE_KPROBE] = "kprobe", 141 + }; 142 + 143 + Return a dictionary with the enum member names as keys and the 144 + associated names as values, for example: 145 + 146 + {'BPF_PROG_TYPE_UNSPEC': 'unspec', 147 + 'BPF_PROG_TYPE_SOCKET_FILTER': 'socket_filter', 148 + 'BPF_PROG_TYPE_KPROBE': 'kprobe'} 149 + 150 + @array_name: name of the array to parse 151 + """ 152 + array_parser = ArrayParser(self.reader, array_name) 153 + array_parser.search_block() 154 + return array_parser.parse() 155 + 156 + def get_enum(self, enum_name): 157 + """ 158 + Search for and parse an enum containing BPF_* members, for example: 159 + 160 + enum bpf_prog_type { 161 + BPF_PROG_TYPE_UNSPEC, 162 + BPF_PROG_TYPE_SOCKET_FILTER, 163 + BPF_PROG_TYPE_KPROBE, 164 + }; 165 + 166 + Return a set containing all member names, for example: 167 + 168 + {'BPF_PROG_TYPE_UNSPEC', 169 + 'BPF_PROG_TYPE_SOCKET_FILTER', 170 + 'BPF_PROG_TYPE_KPROBE'} 171 + 172 + @enum_name: name of the enum to parse 173 + """ 174 + start_marker = re.compile(f'enum {enum_name} {{\n') 175 + pattern = re.compile('^\s*(BPF_\w+),?$') 176 + end_marker = re.compile('^};') 177 + parser = BlockParser(self.reader) 178 + parser.search_block(start_marker) 179 + return parser.parse(pattern, end_marker) 180 + 181 + def __get_description_list(self, start_marker, pattern, end_marker): 182 + parser = InlineListParser(self.reader) 183 + parser.search_block(start_marker) 184 + return parser.parse(pattern, end_marker) 185 + 186 + def get_rst_list(self, block_name): 187 + """ 188 + Search for and parse a list of type names from RST documentation, for 189 + example: 190 + 191 + | *TYPE* := { 192 + | **socket** | **kprobe** | 193 + | **kretprobe** 194 + | } 195 + 196 + Return a set containing all type names, for example: 197 + 198 + {'socket', 'kprobe', 'kretprobe'} 199 + 200 + @block_name: name of the blog to parse, 'TYPE' in the example 201 + """ 202 + start_marker = re.compile(f'\*{block_name}\* := {{') 203 + pattern = re.compile('\*\*([\w/]+)\*\*') 204 + end_marker = re.compile('}\n') 205 + return self.__get_description_list(start_marker, pattern, end_marker) 206 + 207 + def get_help_list(self, block_name): 208 + """ 209 + Search for and parse a list of type names from a help message in 210 + bpftool, for example: 211 + 212 + " TYPE := { socket | kprobe |\\n" 213 + " kretprobe }\\n" 214 + 215 + Return a set containing all type names, for example: 216 + 217 + {'socket', 'kprobe', 'kretprobe'} 218 + 219 + @block_name: name of the blog to parse, 'TYPE' in the example 220 + """ 221 + start_marker = re.compile(f'"\s*{block_name} := {{') 222 + pattern = re.compile('([\w/]+) [|}]') 223 + end_marker = re.compile('}') 224 + return self.__get_description_list(start_marker, pattern, end_marker) 225 + 226 + def get_bashcomp_list(self, block_name): 227 + """ 228 + Search for and parse a list of type names from a variable in bash 229 + completion file, for example: 230 + 231 + local BPFTOOL_PROG_LOAD_TYPES='socket kprobe \\ 232 + kretprobe' 233 + 234 + Return a set containing all type names, for example: 235 + 236 + {'socket', 'kprobe', 'kretprobe'} 237 + 238 + @block_name: name of the blog to parse, 'TYPE' in the example 239 + """ 240 + start_marker = re.compile(f'local {block_name}=\'') 241 + pattern = re.compile('(?:.*=\')?([\w/]+)') 242 + end_marker = re.compile('\'$') 243 + return self.__get_description_list(start_marker, pattern, end_marker) 244 + 245 + class ProgFileExtractor(FileExtractor): 246 + """ 247 + An extractor for bpftool's prog.c. 248 + """ 249 + filename = os.path.join(BPFTOOL_DIR, 'prog.c') 250 + 251 + def get_prog_types(self): 252 + return self.get_types_from_array('prog_type_name') 253 + 254 + def get_attach_types(self): 255 + return self.get_types_from_array('attach_type_strings') 256 + 257 + def get_prog_attach_help(self): 258 + return self.get_help_list('ATTACH_TYPE') 259 + 260 + class MapFileExtractor(FileExtractor): 261 + """ 262 + An extractor for bpftool's map.c. 263 + """ 264 + filename = os.path.join(BPFTOOL_DIR, 'map.c') 265 + 266 + def get_map_types(self): 267 + return self.get_types_from_array('map_type_name') 268 + 269 + def get_map_help(self): 270 + return self.get_help_list('TYPE') 271 + 272 + class CgroupFileExtractor(FileExtractor): 273 + """ 274 + An extractor for bpftool's cgroup.c. 275 + """ 276 + filename = os.path.join(BPFTOOL_DIR, 'cgroup.c') 277 + 278 + def get_prog_attach_help(self): 279 + return self.get_help_list('ATTACH_TYPE') 280 + 281 + class CommonFileExtractor(FileExtractor): 282 + """ 283 + An extractor for bpftool's common.c. 284 + """ 285 + filename = os.path.join(BPFTOOL_DIR, 'common.c') 286 + 287 + def __init__(self): 288 + super().__init__() 289 + self.attach_types = {} 290 + 291 + def get_attach_types(self): 292 + if not self.attach_types: 293 + self.attach_types = self.get_types_from_array('attach_type_name') 294 + return self.attach_types 295 + 296 + def get_cgroup_attach_types(self): 297 + if not self.attach_types: 298 + self.get_attach_types() 299 + cgroup_types = {} 300 + for (key, value) in self.attach_types.items(): 301 + if key.find('BPF_CGROUP') != -1: 302 + cgroup_types[key] = value 303 + return cgroup_types 304 + 305 + class BpfHeaderExtractor(FileExtractor): 306 + """ 307 + An extractor for the UAPI BPF header. 308 + """ 309 + filename = os.path.join(LINUX_ROOT, 'tools/include/uapi/linux/bpf.h') 310 + 311 + def get_prog_types(self): 312 + return self.get_enum('bpf_prog_type') 313 + 314 + def get_map_types(self): 315 + return self.get_enum('bpf_map_type') 316 + 317 + def get_attach_types(self): 318 + return self.get_enum('bpf_attach_type') 319 + 320 + class ManProgExtractor(FileExtractor): 321 + """ 322 + An extractor for bpftool-prog.rst. 323 + """ 324 + filename = os.path.join(BPFTOOL_DIR, 'Documentation/bpftool-prog.rst') 325 + 326 + def get_attach_types(self): 327 + return self.get_rst_list('ATTACH_TYPE') 328 + 329 + class ManMapExtractor(FileExtractor): 330 + """ 331 + An extractor for bpftool-map.rst. 332 + """ 333 + filename = os.path.join(BPFTOOL_DIR, 'Documentation/bpftool-map.rst') 334 + 335 + def get_map_types(self): 336 + return self.get_rst_list('TYPE') 337 + 338 + class ManCgroupExtractor(FileExtractor): 339 + """ 340 + An extractor for bpftool-cgroup.rst. 341 + """ 342 + filename = os.path.join(BPFTOOL_DIR, 'Documentation/bpftool-cgroup.rst') 343 + 344 + def get_attach_types(self): 345 + return self.get_rst_list('ATTACH_TYPE') 346 + 347 + class BashcompExtractor(FileExtractor): 348 + """ 349 + An extractor for bpftool's bash completion file. 350 + """ 351 + filename = os.path.join(BPFTOOL_DIR, 'bash-completion/bpftool') 352 + 353 + def get_prog_attach_types(self): 354 + return self.get_bashcomp_list('BPFTOOL_PROG_ATTACH_TYPES') 355 + 356 + def get_map_types(self): 357 + return self.get_bashcomp_list('BPFTOOL_MAP_CREATE_TYPES') 358 + 359 + def get_cgroup_attach_types(self): 360 + return self.get_bashcomp_list('BPFTOOL_CGROUP_ATTACH_TYPES') 361 + 362 + def verify(first_set, second_set, message): 363 + """ 364 + Print all values that differ between two sets. 365 + @first_set: one set to compare 366 + @second_set: another set to compare 367 + @message: message to print for values belonging to only one of the sets 368 + """ 369 + global retval 370 + diff = first_set.symmetric_difference(second_set) 371 + if diff: 372 + print(message, diff) 373 + retval = 1 374 + 375 + def main(): 376 + # No arguments supported at this time, but print usage for -h|--help 377 + argParser = argparse.ArgumentParser(description=""" 378 + Verify that bpftool's code, help messages, documentation and bash completion 379 + are all in sync on program types, map types and attach types. Also check that 380 + bpftool is in sync with the UAPI BPF header. 381 + """) 382 + args = argParser.parse_args() 383 + 384 + # Map types (enum) 385 + 386 + bpf_info = BpfHeaderExtractor() 387 + ref = bpf_info.get_map_types() 388 + 389 + map_info = MapFileExtractor() 390 + source_map_items = map_info.get_map_types() 391 + map_types_enum = set(source_map_items.keys()) 392 + 393 + verify(ref, map_types_enum, 394 + f'Comparing BPF header (enum bpf_map_type) and {MapFileExtractor.filename} (map_type_name):') 395 + 396 + # Map types (names) 397 + 398 + source_map_types = set(source_map_items.values()) 399 + source_map_types.discard('unspec') 400 + 401 + help_map_types = map_info.get_map_help() 402 + map_info.close() 403 + 404 + man_map_info = ManMapExtractor() 405 + man_map_types = man_map_info.get_map_types() 406 + man_map_info.close() 407 + 408 + bashcomp_info = BashcompExtractor() 409 + bashcomp_map_types = bashcomp_info.get_map_types() 410 + 411 + verify(source_map_types, help_map_types, 412 + f'Comparing {MapFileExtractor.filename} (map_type_name) and {MapFileExtractor.filename} (do_help() TYPE):') 413 + verify(source_map_types, man_map_types, 414 + f'Comparing {MapFileExtractor.filename} (map_type_name) and {ManMapExtractor.filename} (TYPE):') 415 + verify(source_map_types, bashcomp_map_types, 416 + f'Comparing {MapFileExtractor.filename} (map_type_name) and {BashcompExtractor.filename} (BPFTOOL_MAP_CREATE_TYPES):') 417 + 418 + # Program types (enum) 419 + 420 + ref = bpf_info.get_prog_types() 421 + 422 + prog_info = ProgFileExtractor() 423 + prog_types = set(prog_info.get_prog_types().keys()) 424 + 425 + verify(ref, prog_types, 426 + f'Comparing BPF header (enum bpf_prog_type) and {ProgFileExtractor.filename} (prog_type_name):') 427 + 428 + # Attach types (enum) 429 + 430 + ref = bpf_info.get_attach_types() 431 + bpf_info.close() 432 + 433 + common_info = CommonFileExtractor() 434 + attach_types = common_info.get_attach_types() 435 + 436 + verify(ref, attach_types, 437 + f'Comparing BPF header (enum bpf_attach_type) and {CommonFileExtractor.filename} (attach_type_name):') 438 + 439 + # Attach types (names) 440 + 441 + source_prog_attach_types = set(prog_info.get_attach_types().values()) 442 + 443 + help_prog_attach_types = prog_info.get_prog_attach_help() 444 + prog_info.close() 445 + 446 + man_prog_info = ManProgExtractor() 447 + man_prog_attach_types = man_prog_info.get_attach_types() 448 + man_prog_info.close() 449 + 450 + bashcomp_info.reset_read() # We stopped at map types, rewind 451 + bashcomp_prog_attach_types = bashcomp_info.get_prog_attach_types() 452 + 453 + verify(source_prog_attach_types, help_prog_attach_types, 454 + f'Comparing {ProgFileExtractor.filename} (attach_type_strings) and {ProgFileExtractor.filename} (do_help() ATTACH_TYPE):') 455 + verify(source_prog_attach_types, man_prog_attach_types, 456 + f'Comparing {ProgFileExtractor.filename} (attach_type_strings) and {ManProgExtractor.filename} (ATTACH_TYPE):') 457 + verify(source_prog_attach_types, bashcomp_prog_attach_types, 458 + f'Comparing {ProgFileExtractor.filename} (attach_type_strings) and {BashcompExtractor.filename} (BPFTOOL_PROG_ATTACH_TYPES):') 459 + 460 + # Cgroup attach types 461 + 462 + source_cgroup_attach_types = set(common_info.get_cgroup_attach_types().values()) 463 + common_info.close() 464 + 465 + cgroup_info = CgroupFileExtractor() 466 + help_cgroup_attach_types = cgroup_info.get_prog_attach_help() 467 + cgroup_info.close() 468 + 469 + man_cgroup_info = ManCgroupExtractor() 470 + man_cgroup_attach_types = man_cgroup_info.get_attach_types() 471 + man_cgroup_info.close() 472 + 473 + bashcomp_cgroup_attach_types = bashcomp_info.get_cgroup_attach_types() 474 + bashcomp_info.close() 475 + 476 + verify(source_cgroup_attach_types, help_cgroup_attach_types, 477 + f'Comparing {CommonFileExtractor.filename} (attach_type_strings) and {CgroupFileExtractor.filename} (do_help() ATTACH_TYPE):') 478 + verify(source_cgroup_attach_types, man_cgroup_attach_types, 479 + f'Comparing {CommonFileExtractor.filename} (attach_type_strings) and {ManCgroupExtractor.filename} (ATTACH_TYPE):') 480 + verify(source_cgroup_attach_types, bashcomp_cgroup_attach_types, 481 + f'Comparing {CommonFileExtractor.filename} (attach_type_strings) and {BashcompExtractor.filename} (BPFTOOL_CGROUP_ATTACH_TYPES):') 482 + 483 + sys.exit(retval) 484 + 485 + if __name__ == "__main__": 486 + main()