Merge branch 'bpf-fix-null-ptr-derefs-when-showing-a-void-btf-type'

Jiayuan Chen says:

====================
bpf: Fix NULL-ptr-derefs when showing a void BTF type

This series fixes three NULL-ptr-derefs in BTF handling.

Patch 1 handles the syzbot report. A key-less BTF (btf_key_type_id == 0) used
to be rejected for hash maps, until htab and rhtab gained a ->map_check_btf
(to register a dtor) that does not look at the key, so a key-less hash map is
now accepted. Dumping it through bpffs feeds the key type_id 0 into
btf_type_seq_show() and NULL-derefs in btf_type_show(). Reject it again.

Patches 2 and 3 fix two related, pre-existing crashes reachable via
bpf_snprintf_btf(), which renders a type_id taken straight from the BPF
program against the vmlinux BTF. A "const void" (a modifier resolving to
void) NULL-derefs in btf_modifier_show() - void has no ->show op; a
BTF_KIND_VAR NULL-derefs in btf_var_show() - the vmlinux base BTF has no
resolved_ids. Patch 2 falls back to btf_df_show(), the "<unsupported kind:N>"
placeholder already used for FWD/FUNC/FLOAT/DECL_TAG; patch 3 resolves the
var's type directly, mirroring the existing guard in btf_modifier_show().

Patches 4 and 5 add selftests. Patch 4 checks a key-less hash and rhash map is
rejected at creation. Patch 5 extends the snprintf_btf test to render a
"const void" and a BTF_KIND_VAR from the vmlinux BTF and checks they resolve
without crashing.

v3 -> v4: Drop the pin-and-read reproducer from the key-less map test, fold
the void/VAR test into the existing snprintf_btf test, and simplify patch 2's
wording (review comments).

v2 -> v3: Fold in a third fix for the same class of bug, btf_var_show(),
reported while reviewing v2. Address review comments (Fixes attribution,
verbatim syzbot trace, skip instead of fail).

v1 -> v2: AI reported a pre-exist issue. Let's fold it in this series.

v3: https://lore.kernel.org/bpf/20260831110314.150870-1-jiayuan.chen@linux.dev/
v2: https://lore.kernel.org/bpf/20260830073242.148092-1-jiayuan.chen@linux.dev/
v1: https://lore.kernel.org/bpf/20260828093142.179856-1-jiayuan.chen@linux.dev/
====================

Link: https://patch.msgid.link/20260901104924.346187-1-jiayuan.chen@linux.dev
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
This commit is contained in:
Alexei Starovoitov 2026-09-03 09:31:52 -07:00
commit 1329a7aa33
5 changed files with 185 additions and 2 deletions

View File

@ -2911,14 +2911,29 @@ static void btf_modifier_show(const struct btf *btf,
else
t = btf_type_skip_modifiers(btf, type_id, NULL);
btf_type_ops(t)->show(btf, t, type_id, data, bits_offset, show);
/*
* A modifier can resolve to void, which has no show op; print a
* placeholder rather than dereferencing NULL.
*/
if (!btf_type_ops(t))
btf_df_show(btf, t, type_id, data, bits_offset, show);
else
btf_type_ops(t)->show(btf, t, type_id, data, bits_offset, show);
}
static void btf_var_show(const struct btf *btf, const struct btf_type *t,
u32 type_id, void *data, u8 bits_offset,
struct btf_show *show)
{
t = btf_type_id_resolve(btf, &type_id);
/*
* btf_type_id_resolve() dereferences btf->resolved_ids, which is NULL
* for a base BTF (e.g. the vmlinux BTF that bpf_snprintf_btf() uses).
* Resolve the var's type directly in that case.
*/
if (btf->resolved_ids)
t = btf_type_id_resolve(btf, &type_id);
else
t = btf_type_skip_modifiers(btf, t->type, &type_id);
btf_type_ops(t)->show(btf, t, type_id, data, bits_offset, show);
}

View File

@ -530,6 +530,9 @@ static int htab_map_check_btf(struct bpf_map *map, const struct btf *btf,
{
struct bpf_htab *htab = container_of(map, struct bpf_htab, map);
if (btf_type_is_void(key_type))
return -EINVAL;
if (htab_is_prealloc(htab))
return 0;
/*
@ -3111,6 +3114,9 @@ static int rhtab_map_check_btf(struct bpf_map *map, const struct btf *btf,
{
struct bpf_rhtab *rhtab = container_of(map, struct bpf_rhtab, map);
if (btf_type_is_void(key_type))
return -EINVAL;
return bpf_ma_set_dtor(map, &rhtab->ma, rhtab_mem_dtor);
}

View File

@ -0,0 +1,59 @@
// SPDX-License-Identifier: GPL-2.0
#include <test_progs.h>
#include <bpf/btf.h>
/*
* A hash map with a key-less BTF (btf_key_type_id == 0) used to be accepted
* and then NULL-deref in btf_type_show() when dumped through bpffs. A fixed
* kernel rejects it at creation; verify that rejection, with a keyed positive
* control so the -EINVAL is about the missing key type and not some unrelated
* failure.
*/
static void check_keyless(int map_type, __u32 map_flags, int btf_fd, int val_id)
{
LIBBPF_OPTS(bpf_map_create_opts, opts);
int map_fd;
opts.map_flags = map_flags;
opts.btf_fd = btf_fd;
opts.btf_value_type_id = val_id;
/* Positive control: the same map with a real key type is accepted. */
opts.btf_key_type_id = val_id;
map_fd = bpf_map_create(map_type, "keyed_map", 4, 4, 8, &opts);
if (!ASSERT_GE(map_fd, 0, "keyed create is accepted"))
return;
close(map_fd);
/* A key-less BTF must be rejected. */
opts.btf_key_type_id = 0;
map_fd = bpf_map_create(map_type, "keyless_map", 4, 4, 8, &opts);
ASSERT_EQ(map_fd, -EINVAL, "key-less create is rejected");
if (map_fd >= 0)
close(map_fd);
}
void test_btf_map_keyless(void)
{
int btf_fd, val_id;
struct btf *btf;
btf = btf__new_empty();
if (!ASSERT_OK_PTR(btf, "btf__new_empty"))
return;
val_id = btf__add_int(btf, "int", 4, BTF_INT_SIGNED);
if (!ASSERT_GT(val_id, 0, "btf__add_int"))
goto out;
if (!ASSERT_OK(btf__load_into_kernel(btf), "btf__load_into_kernel"))
goto out;
btf_fd = btf__fd(btf);
if (test__start_subtest("hash"))
check_keyless(BPF_MAP_TYPE_HASH, 0, btf_fd, val_id);
if (test__start_subtest("rhash"))
check_keyless(BPF_MAP_TYPE_RHASH, BPF_F_NO_PREALLOC, btf_fd, val_id);
out:
btf__free(btf);
}

View File

@ -1,7 +1,9 @@
// SPDX-License-Identifier: GPL-2.0
#include <test_progs.h>
#include <linux/btf.h>
#include <bpf/btf.h>
#include "netif_receive_skb.skel.h"
#include "snprintf_btf_void.skel.h"
/* Demonstrate that bpf_snprintf_btf succeeds and that various data types
* are formatted correctly.
@ -58,3 +60,80 @@ void serial_test_snprintf_btf(void)
cleanup:
netif_receive_skb__destroy(skel);
}
/*
* bpf_snprintf_btf() renders a type_id taken straight from the vmlinux BTF.
* Two such type_ids used to NULL-deref in the BTF show path:
* - a "const void" (a modifier resolving to void) in btf_modifier_show()
* - a BTF_KIND_VAR in btf_var_show() (base BTF has no resolved_ids)
* A fixed kernel renders both without crashing.
*/
static long run(struct snprintf_btf_void *skel, __u32 type_id)
{
LIBBPF_OPTS(bpf_test_run_opts, topts);
char ctx[8] = {};
skel->bss->type_id = type_id;
topts.ctx_in = ctx;
topts.ctx_size_in = sizeof(ctx);
if (!ASSERT_OK(bpf_prog_test_run_opts(bpf_program__fd(skel->progs.dump_type),
&topts), "test_run"))
return -1;
return skel->bss->ret;
}
void test_snprintf_btf_void(void)
{
const struct btf_type *t;
struct snprintf_btf_void *skel;
int i, n, cv = 0, var = 0;
struct btf *btf;
btf = btf__parse("/sys/kernel/btf/vmlinux", NULL);
if (!btf) {
test__skip();
return;
}
skel = snprintf_btf_void__open_and_load();
if (!ASSERT_OK_PTR(skel, "skel_open_and_load"))
goto out_btf;
n = btf__type_cnt(btf);
for (i = 1; i < n && !(cv && var); i++) {
t = btf__type_by_id(btf, i);
if (!cv && btf_kind(t) == BTF_KIND_CONST && t->type == 0)
cv = i;
/* Pick a VAR small enough to render from the program's buffer. */
if (!var && btf_kind(t) == BTF_KIND_VAR) {
long sz = btf__resolve_size(btf, t->type);
if (sz > 0 && sz <= (long)sizeof(skel->bss->obj))
var = i;
}
}
/* "const void" renders the "<unsupported kind:0>" placeholder. */
if (test__start_subtest("const_void")) {
if (cv) {
ASSERT_EQ(run(skel, cv),
sizeof("<unsupported kind:0>") - 1, "ret");
ASSERT_STREQ(skel->bss->out, "<unsupported kind:0>",
"placeholder");
} else {
test__skip();
}
}
/* A BTF_KIND_VAR must resolve and render without error. */
if (test__start_subtest("var")) {
if (var)
ASSERT_GT(run(skel, var), 0, "ret");
else
test__skip();
}
snprintf_btf_void__destroy(skel);
out_btf:
btf__free(btf);
}

View File

@ -0,0 +1,24 @@
// SPDX-License-Identifier: GPL-2.0
#include "btf_ptr.h"
#include <bpf/bpf_helpers.h>
__u32 type_id;
/* A buffer we own to render the selected type from, kept in bounds. */
char obj[256];
char out[64];
long ret;
SEC("raw_tp/sys_enter")
int dump_type(void *ctx)
{
struct btf_ptr ptr = {
.ptr = obj,
.type_id = type_id,
.flags = 0,
};
ret = bpf_snprintf_btf(out, sizeof(out), &ptr, sizeof(ptr), 0);
return 0;
}
char _license[] SEC("license") = "GPL";