Skip to content

Commit

Permalink
selftests/bpf: add ability to filter programs in veristat
Browse files Browse the repository at this point in the history
Add -f (--filter) argument which accepts glob-based filters for
narrowing down what BPF object files and programs within them should be
processed by veristat. This filtering applies both to comparison and
main (verification) mode.

Filter can be of two forms:
  - file (object) filter: 'strobemeta*'; in this case all the programs
    within matching files are implicitly allowed (or denied, depending
    if it's positive or negative rule, see below);
  - file and prog filter: 'strobemeta*/*unroll*' will further filter
    programs within matching files to only allow those program names that
    match '*unroll*' glob.

As mentioned, filters can be positive (allowlisting) and negative
(denylisting). Negative filters should start with '!': '!strobemeta*'
will deny any filename which basename starts with "strobemeta".

Further, one extra special syntax is supported to allow more convenient
use in practice. Instead of specifying rule on the command line,
veristat allows to specify file that contains rules, both positive and
negative, one line per one filter. This is achieved with -f @<filepath>
use, where <filepath> points to a text file containing rules (negative
and positive rules can be mixed). For convenience empty lines and lines
starting with '#' are ignored. This feature is useful to have some
pre-canned list of object files and program names that are tested
repeatedly, allowing to check in a list of rules and quickly specify
them on the command line.

As a demonstration (and a short cut for nearest future), create a small
list of "interesting" BPF object files from selftests/bpf and commit it
as veristat.cfg. It currently includes 73 programs, most of which are
the most complex and largest BPF programs in selftests, as judged by
total verified instruction count and verifier states total.

If there is overlap between positive or negative filters, negative
filter takes precedence (denylisting is stronger than allowlisting). If
no allow filter is specified, veristat implicitly assumes '*/*' rule. If
no deny rule is specified, veristat (logically) assumes no negative
filters.

Also note that -f (just like -e and -s) can be specified multiple times
and their effect is cumulative.

Signed-off-by: Andrii Nakryiko <[email protected]>
Link: https://lore.kernel.org/r/[email protected]
Signed-off-by: Alexei Starovoitov <[email protected]>
  • Loading branch information
anakryiko authored and Alexei Starovoitov committed Sep 22, 2022
1 parent 394169b commit bde4a96
Show file tree
Hide file tree
Showing 2 changed files with 227 additions and 2 deletions.
212 changes: 210 additions & 2 deletions tools/testing/selftests/bpf/veristat.c
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ enum resfmt {
RESFMT_CSV,
};

struct filter {
char *file_glob;
char *prog_glob;
};

static struct env {
char **filenames;
int filename_cnt;
Expand All @@ -68,6 +73,11 @@ static struct env {

struct stat_specs output_spec;
struct stat_specs sort_spec;

struct filter *allow_filters;
struct filter *deny_filters;
int allow_filter_cnt;
int deny_filter_cnt;
} env;

static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
Expand All @@ -94,10 +104,13 @@ static const struct argp_option opts[] = {
{ "sort", 's', "SPEC", 0, "Specify sort order" },
{ "output-format", 'o', "FMT", 0, "Result output format (table, csv), default is table." },
{ "compare", 'C', NULL, 0, "Comparison mode" },
{ "filter", 'f', "FILTER", 0, "Filter expressions (or @filename for file with expressions)." },
{},
};

static int parse_stats(const char *stats_str, struct stat_specs *specs);
static int append_filter(struct filter **filters, int *cnt, const char *str);
static int append_filter_file(const char *path);

static error_t parse_arg(int key, char *arg, struct argp_state *state)
{
Expand Down Expand Up @@ -134,6 +147,18 @@ static error_t parse_arg(int key, char *arg, struct argp_state *state)
case 'C':
env.comparison_mode = true;
break;
case 'f':
if (arg[0] == '@')
err = append_filter_file(arg + 1);
else if (arg[0] == '!')
err = append_filter(&env.deny_filters, &env.deny_filter_cnt, arg + 1);
else
err = append_filter(&env.allow_filters, &env.allow_filter_cnt, arg);
if (err) {
fprintf(stderr, "Failed to collect program filter expressions: %d\n", err);
return err;
}
break;
case ARGP_KEY_ARG:
tmp = realloc(env.filenames, (env.filename_cnt + 1) * sizeof(*env.filenames));
if (!tmp)
Expand All @@ -156,6 +181,150 @@ static const struct argp argp = {
.doc = argp_program_doc,
};


/* Adapted from perf/util/string.c */
static bool glob_matches(const char *str, const char *pat)
{
while (*str && *pat && *pat != '*') {
if (*str != *pat)
return false;
str++;
pat++;
}
/* Check wild card */
if (*pat == '*') {
while (*pat == '*')
pat++;
if (!*pat) /* Tail wild card matches all */
return true;
while (*str)
if (glob_matches(str++, pat))
return true;
}
return !*str && !*pat;
}

static bool should_process_file(const char *filename)
{
int i;

if (env.deny_filter_cnt > 0) {
for (i = 0; i < env.deny_filter_cnt; i++) {
if (glob_matches(filename, env.deny_filters[i].file_glob))
return false;
}
}

if (env.allow_filter_cnt == 0)
return true;

for (i = 0; i < env.allow_filter_cnt; i++) {
if (glob_matches(filename, env.allow_filters[i].file_glob))
return true;
}

return false;
}

static bool should_process_prog(const char *filename, const char *prog_name)
{
int i;

if (env.deny_filter_cnt > 0) {
for (i = 0; i < env.deny_filter_cnt; i++) {
if (glob_matches(filename, env.deny_filters[i].file_glob))
return false;
if (!env.deny_filters[i].prog_glob)
continue;
if (glob_matches(prog_name, env.deny_filters[i].prog_glob))
return false;
}
}

if (env.allow_filter_cnt == 0)
return true;

for (i = 0; i < env.allow_filter_cnt; i++) {
if (!glob_matches(filename, env.allow_filters[i].file_glob))
continue;
/* if filter specifies only filename glob part, it implicitly
* allows all progs within that file
*/
if (!env.allow_filters[i].prog_glob)
return true;
if (glob_matches(prog_name, env.allow_filters[i].prog_glob))
return true;
}

return false;
}

static int append_filter(struct filter **filters, int *cnt, const char *str)
{
struct filter *f;
void *tmp;
const char *p;

tmp = realloc(*filters, (*cnt + 1) * sizeof(**filters));
if (!tmp)
return -ENOMEM;
*filters = tmp;

f = &(*filters)[*cnt];
f->file_glob = f->prog_glob = NULL;

/* filter can be specified either as "<obj-glob>" or "<obj-glob>/<prog-glob>" */
p = strchr(str, '/');
if (!p) {
f->file_glob = strdup(str);
if (!f->file_glob)
return -ENOMEM;
} else {
f->file_glob = strndup(str, p - str);
f->prog_glob = strdup(p + 1);
if (!f->file_glob || !f->prog_glob) {
free(f->file_glob);
free(f->prog_glob);
f->file_glob = f->prog_glob = NULL;
return -ENOMEM;
}
}

*cnt = *cnt + 1;
return 0;
}

static int append_filter_file(const char *path)
{
char buf[1024];
FILE *f;
int err = 0;

f = fopen(path, "r");
if (!f) {
err = -errno;
fprintf(stderr, "Failed to open '%s': %d\n", path, err);
return err;
}

while (fscanf(f, " %1023[^\n]\n", buf) == 1) {
/* lines starting with # are comments, skip them */
if (buf[0] == '\0' || buf[0] == '#')
continue;
/* lines starting with ! are negative match filters */
if (buf[0] == '!')
err = append_filter(&env.deny_filters, &env.deny_filter_cnt, buf + 1);
else
err = append_filter(&env.allow_filters, &env.allow_filter_cnt, buf);
if (err)
goto cleanup;
}

cleanup:
fclose(f);
return err;
}

static const struct stat_specs default_output_spec = {
.spec_cnt = 7,
.ids = {
Expand Down Expand Up @@ -283,6 +452,9 @@ static int process_prog(const char *filename, struct bpf_object *obj, struct bpf
int err = 0;
void *tmp;

if (!should_process_prog(basename(filename), bpf_program__name(prog)))
return 0;

tmp = realloc(env.prog_stats, (env.prog_stat_cnt + 1) * sizeof(*env.prog_stats));
if (!tmp)
return -ENOMEM;
Expand Down Expand Up @@ -330,6 +502,9 @@ static int process_obj(const char *filename)
LIBBPF_OPTS(bpf_object_open_opts, opts);
int err = 0, prog_cnt = 0;

if (!should_process_file(basename(filename)))
return 0;

old_libbpf_print_fn = libbpf_set_print(libbpf_print_fn);

obj = bpf_object__open_file(filename, &opts);
Expand Down Expand Up @@ -666,7 +841,10 @@ static int parse_stats_csv(const char *filename, struct stat_specs *specs,
goto cleanup;
}
*statsp = tmp;

st = &(*statsp)[*stat_cntp];
memset(st, 0, sizeof(*st));

*stat_cntp += 1;
}

Expand All @@ -692,14 +870,34 @@ static int parse_stats_csv(const char *filename, struct stat_specs *specs,
col++;
}

if (!header && col < specs->spec_cnt) {
if (header) {
header = false;
continue;
}

if (col < specs->spec_cnt) {
fprintf(stderr, "Not enough columns in row #%d in '%s'\n",
*stat_cntp, filename);
err = -EINVAL;
goto cleanup;
}

header = false;
if (!st->file_name || !st->prog_name) {
fprintf(stderr, "Row #%d in '%s' is missing file and/or program name\n",
*stat_cntp, filename);
err = -EINVAL;
goto cleanup;
}

/* in comparison mode we can only check filters after we
* parsed entire line; if row should be ignored we pretend we
* never parsed it
*/
if (!should_process_prog(st->file_name, st->prog_name)) {
free(st->file_name);
free(st->prog_name);
*stat_cntp -= 1;
}
}

if (!feof(f)) {
Expand Down Expand Up @@ -1012,5 +1210,15 @@ int main(int argc, char **argv)
for (i = 0; i < env.filename_cnt; i++)
free(env.filenames[i]);
free(env.filenames);
for (i = 0; i < env.allow_filter_cnt; i++) {
free(env.allow_filters[i].file_glob);
free(env.allow_filters[i].prog_glob);
}
free(env.allow_filters);
for (i = 0; i < env.deny_filter_cnt; i++) {
free(env.deny_filters[i].file_glob);
free(env.deny_filters[i].prog_glob);
}
free(env.deny_filters);
return -err;
}
17 changes: 17 additions & 0 deletions tools/testing/selftests/bpf/veristat.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# pre-canned list of rather complex selftests/bpf BPF object files to monitor
# BPF verifier's performance on
bpf_flow*
bpf_loop_bench*
loop*
netif_receive_skb*
profiler*
pyperf*
strobemeta*
test_cls_redirect*
test_l4lb
test_sysctl*
test_tcp_hdr_*
test_usdt*
test_verif_scale*
test_xdp_noinline*
xdp_synproxy*

0 comments on commit bde4a96

Please sign in to comment.