tools: bpftool: add "prog run" subcommand to test-run programs
authorQuentin Monnet <quentin.monnet@netronome.com>
Fri, 5 Jul 2019 17:54:33 +0000 (18:54 +0100)
committerDaniel Borkmann <daniel@iogearbox.net>
Fri, 5 Jul 2019 21:48:07 +0000 (23:48 +0200)
Add a new "bpftool prog run" subcommand to run a loaded program on input
data (and possibly with input context) passed by the user.

Print output data (and output context if relevant) into a file or into
the console. Print return value and duration for the test run into the
console.

A "repeat" argument can be passed to run the program several times in a
row.

The command does not perform any kind of verification based on program
type (Is this program type allowed to use an input context?) or on data
consistency (Can I work with empty input data?), this is left to the
kernel.

Example invocation:

    # perl -e 'print "\x0" x 14' | ./bpftool prog run \
            pinned /sys/fs/bpf/sample_ret0 \
            data_in - data_out - repeat 5
    0000000 0000 0000 0000 0000 0000 0000 0000      | ........ ......
    Return value: 0, duration (average): 260ns

When one of data_in or ctx_in is "-", bpftool reads from standard input,
in binary format. Other formats (JSON, hexdump) might be supported (via
an optional command line keyword like "data_fmt_in") in the future if
relevant, but this would require doing more parsing in bpftool.

v2:
- Fix argument names for function check_single_stdin(). (Yonghong)

Signed-off-by: Quentin Monnet <quentin.monnet@netronome.com>
Reviewed-by: Jakub Kicinski <jakub.kicinski@netronome.com>
Acked-by: Yonghong Song <yhs@fb.com>
Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
tools/bpf/bpftool/Documentation/bpftool-prog.rst
tools/bpf/bpftool/bash-completion/bpftool
tools/bpf/bpftool/main.c
tools/bpf/bpftool/main.h
tools/bpf/bpftool/prog.c
tools/include/linux/sizes.h [new file with mode: 0644]

index 1df637f..7a374b3 100644 (file)
@@ -29,6 +29,7 @@ PROG COMMANDS
 |      **bpftool** **prog attach** *PROG* *ATTACH_TYPE* [*MAP*]
 |      **bpftool** **prog detach** *PROG* *ATTACH_TYPE* [*MAP*]
 |      **bpftool** **prog tracelog**
+|      **bpftool** **prog run** *PROG* **data_in** *FILE* [**data_out** *FILE* [**data_size_out** *L*]] [**ctx_in** *FILE* [**ctx_out** *FILE* [**ctx_size_out** *M*]]] [**repeat** *N*]
 |      **bpftool** **prog help**
 |
 |      *MAP* := { **id** *MAP_ID* | **pinned** *FILE* }
@@ -146,6 +147,39 @@ DESCRIPTION
                  streaming data from BPF programs to user space, one can use
                  perf events (see also **bpftool-map**\ (8)).
 
+       **bpftool prog run** *PROG* **data_in** *FILE* [**data_out** *FILE* [**data_size_out** *L*]] [**ctx_in** *FILE* [**ctx_out** *FILE* [**ctx_size_out** *M*]]] [**repeat** *N*]
+                 Run BPF program *PROG* in the kernel testing infrastructure
+                 for BPF, meaning that the program works on the data and
+                 context provided by the user, and not on actual packets or
+                 monitored functions etc. Return value and duration for the
+                 test run are printed out to the console.
+
+                 Input data is read from the *FILE* passed with **data_in**.
+                 If this *FILE* is "**-**", input data is read from standard
+                 input. Input context, if any, is read from *FILE* passed with
+                 **ctx_in**. Again, "**-**" can be used to read from standard
+                 input, but only if standard input is not already in use for
+                 input data. If a *FILE* is passed with **data_out**, output
+                 data is written to that file. Similarly, output context is
+                 written to the *FILE* passed with **ctx_out**. For both
+                 output flows, "**-**" can be used to print to the standard
+                 output (as plain text, or JSON if relevant option was
+                 passed). If output keywords are omitted, output data and
+                 context are discarded. Keywords **data_size_out** and
+                 **ctx_size_out** are used to pass the size (in bytes) for the
+                 output buffers to the kernel, although the default of 32 kB
+                 should be more than enough for most cases.
+
+                 Keyword **repeat** is used to indicate the number of
+                 consecutive runs to perform. Note that output data and
+                 context printed to files correspond to the last of those
+                 runs. The duration printed out at the end of the runs is an
+                 average over all runs performed by the command.
+
+                 Not all program types support test run. Among those which do,
+                 not all of them can take the **ctx_in**/**ctx_out**
+                 arguments. bpftool does not perform checks on program types.
+
        **bpftool prog help**
                  Print short help message.
 
index ba37095..965a865 100644 (file)
@@ -408,10 +408,34 @@ _bpftool()
                 tracelog)
                     return 0
                     ;;
+                run)
+                    if [[ ${#words[@]} -lt 5 ]]; then
+                        _filedir
+                        return 0
+                    fi
+                    case $prev in
+                        id)
+                            _bpftool_get_prog_ids
+                            return 0
+                            ;;
+                        data_in|data_out|ctx_in|ctx_out)
+                            _filedir
+                            return 0
+                            ;;
+                        repeat|data_size_out|ctx_size_out)
+                            return 0
+                            ;;
+                        *)
+                            _bpftool_once_attr 'data_in data_out data_size_out \
+                                ctx_in ctx_out ctx_size_out repeat'
+                            return 0
+                            ;;
+                    esac
+                    ;;
                 *)
                     [[ $prev == $object ]] && \
-                        COMPREPLY=( $( compgen -W 'dump help pin attach detach load \
-                            show list tracelog' -- "$cur" ) )
+                        COMPREPLY=( $( compgen -W 'dump help pin attach detach \
+                            load show list tracelog run' -- "$cur" ) )
                     ;;
             esac
             ;;
index 4879f63..e916ff2 100644 (file)
@@ -117,6 +117,35 @@ bool is_prefix(const char *pfx, const char *str)
        return !memcmp(str, pfx, strlen(pfx));
 }
 
+/* Last argument MUST be NULL pointer */
+int detect_common_prefix(const char *arg, ...)
+{
+       unsigned int count = 0;
+       const char *ref;
+       char msg[256];
+       va_list ap;
+
+       snprintf(msg, sizeof(msg), "ambiguous prefix: '%s' could be '", arg);
+       va_start(ap, arg);
+       while ((ref = va_arg(ap, const char *))) {
+               if (!is_prefix(arg, ref))
+                       continue;
+               count++;
+               if (count > 1)
+                       strncat(msg, "' or '", sizeof(msg) - strlen(msg) - 1);
+               strncat(msg, ref, sizeof(msg) - strlen(msg) - 1);
+       }
+       va_end(ap);
+       strncat(msg, "'", sizeof(msg) - strlen(msg) - 1);
+
+       if (count >= 2) {
+               p_err(msg);
+               return -1;
+       }
+
+       return 0;
+}
+
 void fprint_hex(FILE *f, void *arg, unsigned int n, const char *sep)
 {
        unsigned char *data = arg;
index 9c5d9c8..3ef0d90 100644 (file)
@@ -101,6 +101,7 @@ void p_err(const char *fmt, ...);
 void p_info(const char *fmt, ...);
 
 bool is_prefix(const char *pfx, const char *str);
+int detect_common_prefix(const char *arg, ...);
 void fprint_hex(FILE *f, void *arg, unsigned int n, const char *sep);
 void usage(void) __noreturn;
 
index 9b0db5d..66f04a4 100644 (file)
@@ -15,6 +15,7 @@
 #include <sys/stat.h>
 
 #include <linux/err.h>
+#include <linux/sizes.h>
 
 #include <bpf.h>
 #include <btf.h>
@@ -748,6 +749,344 @@ static int do_detach(int argc, char **argv)
        return 0;
 }
 
+static int check_single_stdin(char *file_data_in, char *file_ctx_in)
+{
+       if (file_data_in && file_ctx_in &&
+           !strcmp(file_data_in, "-") && !strcmp(file_ctx_in, "-")) {
+               p_err("cannot use standard input for both data_in and ctx_in");
+               return -1;
+       }
+
+       return 0;
+}
+
+static int get_run_data(const char *fname, void **data_ptr, unsigned int *size)
+{
+       size_t block_size = 256;
+       size_t buf_size = block_size;
+       size_t nb_read = 0;
+       void *tmp;
+       FILE *f;
+
+       if (!fname) {
+               *data_ptr = NULL;
+               *size = 0;
+               return 0;
+       }
+
+       if (!strcmp(fname, "-"))
+               f = stdin;
+       else
+               f = fopen(fname, "r");
+       if (!f) {
+               p_err("failed to open %s: %s", fname, strerror(errno));
+               return -1;
+       }
+
+       *data_ptr = malloc(block_size);
+       if (!*data_ptr) {
+               p_err("failed to allocate memory for data_in/ctx_in: %s",
+                     strerror(errno));
+               goto err_fclose;
+       }
+
+       while ((nb_read += fread(*data_ptr + nb_read, 1, block_size, f))) {
+               if (feof(f))
+                       break;
+               if (ferror(f)) {
+                       p_err("failed to read data_in/ctx_in from %s: %s",
+                             fname, strerror(errno));
+                       goto err_free;
+               }
+               if (nb_read > buf_size - block_size) {
+                       if (buf_size == UINT32_MAX) {
+                               p_err("data_in/ctx_in is too long (max: %d)",
+                                     UINT32_MAX);
+                               goto err_free;
+                       }
+                       /* No space for fread()-ing next chunk; realloc() */
+                       buf_size *= 2;
+                       tmp = realloc(*data_ptr, buf_size);
+                       if (!tmp) {
+                               p_err("failed to reallocate data_in/ctx_in: %s",
+                                     strerror(errno));
+                               goto err_free;
+                       }
+                       *data_ptr = tmp;
+               }
+       }
+       if (f != stdin)
+               fclose(f);
+
+       *size = nb_read;
+       return 0;
+
+err_free:
+       free(*data_ptr);
+       *data_ptr = NULL;
+err_fclose:
+       if (f != stdin)
+               fclose(f);
+       return -1;
+}
+
+static void hex_print(void *data, unsigned int size, FILE *f)
+{
+       size_t i, j;
+       char c;
+
+       for (i = 0; i < size; i += 16) {
+               /* Row offset */
+               fprintf(f, "%07zx\t", i);
+
+               /* Hexadecimal values */
+               for (j = i; j < i + 16 && j < size; j++)
+                       fprintf(f, "%02x%s", *(uint8_t *)(data + j),
+                               j % 2 ? " " : "");
+               for (; j < i + 16; j++)
+                       fprintf(f, "  %s", j % 2 ? " " : "");
+
+               /* ASCII values (if relevant), '.' otherwise */
+               fprintf(f, "| ");
+               for (j = i; j < i + 16 && j < size; j++) {
+                       c = *(char *)(data + j);
+                       if (c < ' ' || c > '~')
+                               c = '.';
+                       fprintf(f, "%c%s", c, j == i + 7 ? " " : "");
+               }
+
+               fprintf(f, "\n");
+       }
+}
+
+static int
+print_run_output(void *data, unsigned int size, const char *fname,
+                const char *json_key)
+{
+       size_t nb_written;
+       FILE *f;
+
+       if (!fname)
+               return 0;
+
+       if (!strcmp(fname, "-")) {
+               f = stdout;
+               if (json_output) {
+                       jsonw_name(json_wtr, json_key);
+                       print_data_json(data, size);
+               } else {
+                       hex_print(data, size, f);
+               }
+               return 0;
+       }
+
+       f = fopen(fname, "w");
+       if (!f) {
+               p_err("failed to open %s: %s", fname, strerror(errno));
+               return -1;
+       }
+
+       nb_written = fwrite(data, 1, size, f);
+       fclose(f);
+       if (nb_written != size) {
+               p_err("failed to write output data/ctx: %s", strerror(errno));
+               return -1;
+       }
+
+       return 0;
+}
+
+static int alloc_run_data(void **data_ptr, unsigned int size_out)
+{
+       *data_ptr = calloc(size_out, 1);
+       if (!*data_ptr) {
+               p_err("failed to allocate memory for output data/ctx: %s",
+                     strerror(errno));
+               return -1;
+       }
+
+       return 0;
+}
+
+static int do_run(int argc, char **argv)
+{
+       char *data_fname_in = NULL, *data_fname_out = NULL;
+       char *ctx_fname_in = NULL, *ctx_fname_out = NULL;
+       struct bpf_prog_test_run_attr test_attr = {0};
+       const unsigned int default_size = SZ_32K;
+       void *data_in = NULL, *data_out = NULL;
+       void *ctx_in = NULL, *ctx_out = NULL;
+       unsigned int repeat = 1;
+       int fd, err;
+
+       if (!REQ_ARGS(4))
+               return -1;
+
+       fd = prog_parse_fd(&argc, &argv);
+       if (fd < 0)
+               return -1;
+
+       while (argc) {
+               if (detect_common_prefix(*argv, "data_in", "data_out",
+                                        "data_size_out", NULL))
+                       return -1;
+               if (detect_common_prefix(*argv, "ctx_in", "ctx_out",
+                                        "ctx_size_out", NULL))
+                       return -1;
+
+               if (is_prefix(*argv, "data_in")) {
+                       NEXT_ARG();
+                       if (!REQ_ARGS(1))
+                               return -1;
+
+                       data_fname_in = GET_ARG();
+                       if (check_single_stdin(data_fname_in, ctx_fname_in))
+                               return -1;
+               } else if (is_prefix(*argv, "data_out")) {
+                       NEXT_ARG();
+                       if (!REQ_ARGS(1))
+                               return -1;
+
+                       data_fname_out = GET_ARG();
+               } else if (is_prefix(*argv, "data_size_out")) {
+                       char *endptr;
+
+                       NEXT_ARG();
+                       if (!REQ_ARGS(1))
+                               return -1;
+
+                       test_attr.data_size_out = strtoul(*argv, &endptr, 0);
+                       if (*endptr) {
+                               p_err("can't parse %s as output data size",
+                                     *argv);
+                               return -1;
+                       }
+                       NEXT_ARG();
+               } else if (is_prefix(*argv, "ctx_in")) {
+                       NEXT_ARG();
+                       if (!REQ_ARGS(1))
+                               return -1;
+
+                       ctx_fname_in = GET_ARG();
+                       if (check_single_stdin(data_fname_in, ctx_fname_in))
+                               return -1;
+               } else if (is_prefix(*argv, "ctx_out")) {
+                       NEXT_ARG();
+                       if (!REQ_ARGS(1))
+                               return -1;
+
+                       ctx_fname_out = GET_ARG();
+               } else if (is_prefix(*argv, "ctx_size_out")) {
+                       char *endptr;
+
+                       NEXT_ARG();
+                       if (!REQ_ARGS(1))
+                               return -1;
+
+                       test_attr.ctx_size_out = strtoul(*argv, &endptr, 0);
+                       if (*endptr) {
+                               p_err("can't parse %s as output context size",
+                                     *argv);
+                               return -1;
+                       }
+                       NEXT_ARG();
+               } else if (is_prefix(*argv, "repeat")) {
+                       char *endptr;
+
+                       NEXT_ARG();
+                       if (!REQ_ARGS(1))
+                               return -1;
+
+                       repeat = strtoul(*argv, &endptr, 0);
+                       if (*endptr) {
+                               p_err("can't parse %s as repeat number",
+                                     *argv);
+                               return -1;
+                       }
+                       NEXT_ARG();
+               } else {
+                       p_err("expected no more arguments, 'data_in', 'data_out', 'data_size_out', 'ctx_in', 'ctx_out', 'ctx_size_out' or 'repeat', got: '%s'?",
+                             *argv);
+                       return -1;
+               }
+       }
+
+       err = get_run_data(data_fname_in, &data_in, &test_attr.data_size_in);
+       if (err)
+               return -1;
+
+       if (data_in) {
+               if (!test_attr.data_size_out)
+                       test_attr.data_size_out = default_size;
+               err = alloc_run_data(&data_out, test_attr.data_size_out);
+               if (err)
+                       goto free_data_in;
+       }
+
+       err = get_run_data(ctx_fname_in, &ctx_in, &test_attr.ctx_size_in);
+       if (err)
+               goto free_data_out;
+
+       if (ctx_in) {
+               if (!test_attr.ctx_size_out)
+                       test_attr.ctx_size_out = default_size;
+               err = alloc_run_data(&ctx_out, test_attr.ctx_size_out);
+               if (err)
+                       goto free_ctx_in;
+       }
+
+       test_attr.prog_fd       = fd;
+       test_attr.repeat        = repeat;
+       test_attr.data_in       = data_in;
+       test_attr.data_out      = data_out;
+       test_attr.ctx_in        = ctx_in;
+       test_attr.ctx_out       = ctx_out;
+
+       err = bpf_prog_test_run_xattr(&test_attr);
+       if (err) {
+               p_err("failed to run program: %s", strerror(errno));
+               goto free_ctx_out;
+       }
+
+       err = 0;
+
+       if (json_output)
+               jsonw_start_object(json_wtr);   /* root */
+
+       /* Do not exit on errors occurring when printing output data/context,
+        * we still want to print return value and duration for program run.
+        */
+       if (test_attr.data_size_out)
+               err += print_run_output(test_attr.data_out,
+                                       test_attr.data_size_out,
+                                       data_fname_out, "data_out");
+       if (test_attr.ctx_size_out)
+               err += print_run_output(test_attr.ctx_out,
+                                       test_attr.ctx_size_out,
+                                       ctx_fname_out, "ctx_out");
+
+       if (json_output) {
+               jsonw_uint_field(json_wtr, "retval", test_attr.retval);
+               jsonw_uint_field(json_wtr, "duration", test_attr.duration);
+               jsonw_end_object(json_wtr);     /* root */
+       } else {
+               fprintf(stdout, "Return value: %u, duration%s: %uns\n",
+                       test_attr.retval,
+                       repeat > 1 ? " (average)" : "", test_attr.duration);
+       }
+
+free_ctx_out:
+       free(ctx_out);
+free_ctx_in:
+       free(ctx_in);
+free_data_out:
+       free(data_out);
+free_data_in:
+       free(data_in);
+
+       return err;
+}
+
 static int load_with_options(int argc, char **argv, bool first_prog_only)
 {
        struct bpf_object_load_attr load_attr = { 0 };
@@ -1058,6 +1397,11 @@ static int do_help(int argc, char **argv)
                "                         [pinmaps MAP_DIR]\n"
                "       %s %s attach PROG ATTACH_TYPE [MAP]\n"
                "       %s %s detach PROG ATTACH_TYPE [MAP]\n"
+               "       %s %s run PROG \\\n"
+               "                         data_in FILE \\\n"
+               "                         [data_out FILE [data_size_out L]] \\\n"
+               "                         [ctx_in FILE [ctx_out FILE [ctx_size_out M]]] \\\n"
+               "                         [repeat N]\n"
                "       %s %s tracelog\n"
                "       %s %s help\n"
                "\n"
@@ -1079,7 +1423,8 @@ static int do_help(int argc, char **argv)
                "",
                bin_name, argv[-2], bin_name, argv[-2], bin_name, argv[-2],
                bin_name, argv[-2], bin_name, argv[-2], bin_name, argv[-2],
-               bin_name, argv[-2], bin_name, argv[-2], bin_name, argv[-2]);
+               bin_name, argv[-2], bin_name, argv[-2], bin_name, argv[-2],
+               bin_name, argv[-2]);
 
        return 0;
 }
@@ -1095,6 +1440,7 @@ static const struct cmd cmds[] = {
        { "attach",     do_attach },
        { "detach",     do_detach },
        { "tracelog",   do_tracelog },
+       { "run",        do_run },
        { 0 }
 };
 
diff --git a/tools/include/linux/sizes.h b/tools/include/linux/sizes.h
new file mode 100644 (file)
index 0000000..1cbb4c4
--- /dev/null
@@ -0,0 +1,48 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * include/linux/sizes.h
+ */
+#ifndef __LINUX_SIZES_H__
+#define __LINUX_SIZES_H__
+
+#include <linux/const.h>
+
+#define SZ_1                           0x00000001
+#define SZ_2                           0x00000002
+#define SZ_4                           0x00000004
+#define SZ_8                           0x00000008
+#define SZ_16                          0x00000010
+#define SZ_32                          0x00000020
+#define SZ_64                          0x00000040
+#define SZ_128                         0x00000080
+#define SZ_256                         0x00000100
+#define SZ_512                         0x00000200
+
+#define SZ_1K                          0x00000400
+#define SZ_2K                          0x00000800
+#define SZ_4K                          0x00001000
+#define SZ_8K                          0x00002000
+#define SZ_16K                         0x00004000
+#define SZ_32K                         0x00008000
+#define SZ_64K                         0x00010000
+#define SZ_128K                                0x00020000
+#define SZ_256K                                0x00040000
+#define SZ_512K                                0x00080000
+
+#define SZ_1M                          0x00100000
+#define SZ_2M                          0x00200000
+#define SZ_4M                          0x00400000
+#define SZ_8M                          0x00800000
+#define SZ_16M                         0x01000000
+#define SZ_32M                         0x02000000
+#define SZ_64M                         0x04000000
+#define SZ_128M                                0x08000000
+#define SZ_256M                                0x10000000
+#define SZ_512M                                0x20000000
+
+#define SZ_1G                          0x40000000
+#define SZ_2G                          0x80000000
+
+#define SZ_4G                          _AC(0x100000000, ULL)
+
+#endif /* __LINUX_SIZES_H__ */