Commit 09683080 authored by Alexei Starovoitov's avatar Alexei Starovoitov
Browse files

Merge branch 'Fix reference state management for synchronous callbacks'

Kumar Kartikeya Dwivedi says:

====================

This is patch 1, 2 + their individual tests split into a separate series from
the RFC, so that these can be taken in, while we continue working towards a fix
for handling stack access inside the callback.

Changelog:
----------
v1 -> v2:
v1: https://lore.kernel.org/bpf/20220822131923.21476-1-memxor@gmail.com

  * Fix error for test_progs-no_alu32 due to distinct alloc_insn in errstr

RFC v1 -> v1:
RFC v1: https://lore.kernel.org/bpf/20220815051540.18791-1-memxor@gmail.com



  * Fix up commit log to add more explanation (Alexei)
  * Split reference state fix out into a separate series
====================

Signed-off-by: default avatarAlexei Starovoitov <ast@kernel.org>
parents f52c8947 35f14dbd
Loading
Loading
Loading
Loading
+11 −0
Original line number Diff line number Diff line
@@ -212,6 +212,17 @@ struct bpf_reference_state {
	 * is used purely to inform the user of a reference leak.
	 */
	int insn_idx;
	/* There can be a case like:
	 * main (frame 0)
	 *  cb (frame 1)
	 *   func (frame 3)
	 *    cb (frame 4)
	 * Hence for frame 4, if callback_ref just stored boolean, it would be
	 * impossible to distinguish nested callback refs. Hence store the
	 * frameno and compare that to callback_ref in check_reference_leak when
	 * exiting a callback function.
	 */
	int callback_ref;
};

/* state of the program:
+4 −4
Original line number Diff line number Diff line
@@ -1613,10 +1613,6 @@ bpf_base_func_proto(enum bpf_func_id func_id)
		return &bpf_ringbuf_submit_dynptr_proto;
	case BPF_FUNC_ringbuf_discard_dynptr:
		return &bpf_ringbuf_discard_dynptr_proto;
	case BPF_FUNC_for_each_map_elem:
		return &bpf_for_each_map_elem_proto;
	case BPF_FUNC_loop:
		return &bpf_loop_proto;
	case BPF_FUNC_strncmp:
		return &bpf_strncmp_proto;
	case BPF_FUNC_strtol:
@@ -1659,6 +1655,10 @@ bpf_base_func_proto(enum bpf_func_id func_id)
		return &bpf_timer_cancel_proto;
	case BPF_FUNC_kptr_xchg:
		return &bpf_kptr_xchg_proto;
	case BPF_FUNC_for_each_map_elem:
		return &bpf_for_each_map_elem_proto;
	case BPF_FUNC_loop:
		return &bpf_loop_proto;
	default:
		break;
	}
+33 −9
Original line number Diff line number Diff line
@@ -1092,6 +1092,7 @@ static int acquire_reference_state(struct bpf_verifier_env *env, int insn_idx)
	id = ++env->id_gen;
	state->refs[new_ofs].id = id;
	state->refs[new_ofs].insn_idx = insn_idx;
	state->refs[new_ofs].callback_ref = state->in_callback_fn ? state->frameno : 0;

	return id;
}
@@ -1104,6 +1105,9 @@ static int release_reference_state(struct bpf_func_state *state, int ptr_id)
	last_idx = state->acquired_refs - 1;
	for (i = 0; i < state->acquired_refs; i++) {
		if (state->refs[i].id == ptr_id) {
			/* Cannot release caller references in callbacks */
			if (state->in_callback_fn && state->refs[i].callback_ref != state->frameno)
				return -EINVAL;
			if (last_idx && i != last_idx)
				memcpy(&state->refs[i], &state->refs[last_idx],
				       sizeof(*state->refs));
@@ -6915,10 +6919,17 @@ static int prepare_func_exit(struct bpf_verifier_env *env, int *insn_idx)
		caller->regs[BPF_REG_0] = *r0;
	}

	/* callback_fn frame should have released its own additions to parent's
	 * reference state at this point, or check_reference_leak would
	 * complain, hence it must be the same as the caller. There is no need
	 * to copy it back.
	 */
	if (!callee->in_callback_fn) {
		/* Transfer references to the caller */
		err = copy_reference_state(caller, callee);
		if (err)
			return err;
	}

	*insn_idx = callee->callsite + 1;
	if (env->log.level & BPF_LOG_LEVEL) {
@@ -7042,13 +7053,20 @@ record_func_key(struct bpf_verifier_env *env, struct bpf_call_arg_meta *meta,
static int check_reference_leak(struct bpf_verifier_env *env)
{
	struct bpf_func_state *state = cur_func(env);
	bool refs_lingering = false;
	int i;

	if (state->frameno && !state->in_callback_fn)
		return 0;

	for (i = 0; i < state->acquired_refs; i++) {
		if (state->in_callback_fn && state->refs[i].callback_ref != state->frameno)
			continue;
		verbose(env, "Unreleased reference id=%d alloc_insn=%d\n",
			state->refs[i].id, state->refs[i].insn_idx);
		refs_lingering = true;
	}
	return state->acquired_refs ? -EINVAL : 0;
	return refs_lingering ? -EINVAL : 0;
}

static int check_bpf_snprintf_call(struct bpf_verifier_env *env,
@@ -12337,6 +12355,16 @@ static int do_check(struct bpf_verifier_env *env)
					return -EINVAL;
				}

				/* We must do check_reference_leak here before
				 * prepare_func_exit to handle the case when
				 * state->curframe > 0, it may be a callback
				 * function, for which reference_state must
				 * match caller reference state when it exits.
				 */
				err = check_reference_leak(env);
				if (err)
					return err;

				if (state->curframe) {
					/* exit from nested function */
					err = prepare_func_exit(env, &env->insn_idx);
@@ -12346,10 +12374,6 @@ static int do_check(struct bpf_verifier_env *env)
					continue;
				}

				err = check_reference_leak(env);
				if (err)
					return err;

				err = check_return_code(env);
				if (err)
					return err;
+48 −0
Original line number Diff line number Diff line
// SPDX-License-Identifier: GPL-2.0
#include "bpf/libbpf.h"
#include <test_progs.h>
#include <network_helpers.h>

#include "cb_refs.skel.h"

static char log_buf[1024 * 1024];

struct {
	const char *prog_name;
	const char *err_msg;
} cb_refs_tests[] = {
	{ "underflow_prog", "reference has not been acquired before" },
	{ "leak_prog", "Unreleased reference" },
	{ "nested_cb", "Unreleased reference id=4 alloc_insn=2" }, /* alloc_insn=2{4,5} */
	{ "non_cb_transfer_ref", "Unreleased reference id=4 alloc_insn=1" }, /* alloc_insn=1{1,2} */
};

void test_cb_refs(void)
{
	LIBBPF_OPTS(bpf_object_open_opts, opts, .kernel_log_buf = log_buf,
						.kernel_log_size = sizeof(log_buf),
						.kernel_log_level = 1);
	struct bpf_program *prog;
	struct cb_refs *skel;
	int i;

	for (i = 0; i < ARRAY_SIZE(cb_refs_tests); i++) {
		LIBBPF_OPTS(bpf_test_run_opts, run_opts,
			.data_in = &pkt_v4,
			.data_size_in = sizeof(pkt_v4),
			.repeat = 1,
		);
		skel = cb_refs__open_opts(&opts);
		if (!ASSERT_OK_PTR(skel, "cb_refs__open_and_load"))
			return;
		prog = bpf_object__find_program_by_name(skel->obj, cb_refs_tests[i].prog_name);
		bpf_program__set_autoload(prog, true);
		if (!ASSERT_ERR(cb_refs__load(skel), "cb_refs__load"))
			bpf_prog_test_run_opts(bpf_program__fd(prog), &run_opts);
		if (!ASSERT_OK_PTR(strstr(log_buf, cb_refs_tests[i].err_msg), "expected error message")) {
			fprintf(stderr, "Expected: %s\n", cb_refs_tests[i].err_msg);
			fprintf(stderr, "Verifier: %s\n", log_buf);
		}
		cb_refs__destroy(skel);
	}
}
+116 −0
Original line number Diff line number Diff line
// SPDX-License-Identifier: GPL-2.0
#include <vmlinux.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_helpers.h>

struct map_value {
	struct prog_test_ref_kfunc __kptr_ref *ptr;
};

struct {
	__uint(type, BPF_MAP_TYPE_ARRAY);
	__type(key, int);
	__type(value, struct map_value);
	__uint(max_entries, 16);
} array_map SEC(".maps");

extern struct prog_test_ref_kfunc *bpf_kfunc_call_test_acquire(unsigned long *sp) __ksym;
extern void bpf_kfunc_call_test_release(struct prog_test_ref_kfunc *p) __ksym;

static __noinline int cb1(void *map, void *key, void *value, void *ctx)
{
	void *p = *(void **)ctx;
	bpf_kfunc_call_test_release(p);
	/* Without the fix this would cause underflow */
	return 0;
}

SEC("?tc")
int underflow_prog(void *ctx)
{
	struct prog_test_ref_kfunc *p;
	unsigned long sl = 0;

	p = bpf_kfunc_call_test_acquire(&sl);
	if (!p)
		return 0;
	bpf_for_each_map_elem(&array_map, cb1, &p, 0);
	return 0;
}

static __always_inline int cb2(void *map, void *key, void *value, void *ctx)
{
	unsigned long sl = 0;

	*(void **)ctx = bpf_kfunc_call_test_acquire(&sl);
	/* Without the fix this would leak memory */
	return 0;
}

SEC("?tc")
int leak_prog(void *ctx)
{
	struct prog_test_ref_kfunc *p;
	struct map_value *v;
	unsigned long sl;

	v = bpf_map_lookup_elem(&array_map, &(int){0});
	if (!v)
		return 0;

	p = NULL;
	bpf_for_each_map_elem(&array_map, cb2, &p, 0);
	p = bpf_kptr_xchg(&v->ptr, p);
	if (p)
		bpf_kfunc_call_test_release(p);
	return 0;
}

static __always_inline int cb(void *map, void *key, void *value, void *ctx)
{
	return 0;
}

static __always_inline int cb3(void *map, void *key, void *value, void *ctx)
{
	unsigned long sl = 0;
	void *p;

	bpf_kfunc_call_test_acquire(&sl);
	bpf_for_each_map_elem(&array_map, cb, &p, 0);
	/* It should only complain here, not in cb. This is why we need
	 * callback_ref to be set to frameno.
	 */
	return 0;
}

SEC("?tc")
int nested_cb(void *ctx)
{
	struct prog_test_ref_kfunc *p;
	unsigned long sl = 0;
	int sp = 0;

	p = bpf_kfunc_call_test_acquire(&sl);
	if (!p)
		return 0;
	bpf_for_each_map_elem(&array_map, cb3, &sp, 0);
	bpf_kfunc_call_test_release(p);
	return 0;
}

SEC("?tc")
int non_cb_transfer_ref(void *ctx)
{
	struct prog_test_ref_kfunc *p;
	unsigned long sl = 0;

	p = bpf_kfunc_call_test_acquire(&sl);
	if (!p)
		return 0;
	cb1(NULL, NULL, NULL, &p);
	bpf_kfunc_call_test_acquire(&sl);
	return 0;
}

char _license[] SEC("license") = "GPL";