Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ba95c745 authored by Quentin Monnet's avatar Quentin Monnet Committed by Daniel Borkmann
Browse files

tools: bpftool: add "prog run" subcommand to test-run programs



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: default avatarQuentin Monnet <quentin.monnet@netronome.com>
Reviewed-by: default avatarJakub Kicinski <jakub.kicinski@netronome.com>
Acked-by: default avatarYonghong Song <yhs@fb.com>
Signed-off-by: default avatarDaniel Borkmann <daniel@iogearbox.net>
parent e232cb6f
Loading
Loading
Loading
Loading
+34 −0
Original line number Diff line number Diff line
@@ -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.

+26 −2
Original line number Diff line number Diff line
@@ -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
            ;;
+29 −0
Original line number Diff line number Diff line
@@ -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;
+1 −0
Original line number Diff line number Diff line
@@ -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;

+347 −1
Original line number Diff line number Diff line
@@ -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 }
};

Loading