* [PATCH bpf-next 2/2] selftests/bpf: Cover recursive bpftool map dumps
2026-09-06 14:39 [PATCH bpf-next 1/2] bpftool: Add recursive map dumping Tianyi Chen
@ 2026-09-06 14:39 ` Tianyi Chen
2026-09-06 15:29 ` [PATCH bpf-next 1/2] bpftool: Add recursive map dumping bot+bpf-ci
1 sibling, 0 replies; 4+ messages in thread
From: Tianyi Chen @ 2026-09-06 14:39 UTC (permalink / raw)
To: qmo, Andrii Nakryiko, Eduard Zingerman
Cc: Tianyi Chen, Alexei Starovoitov, Daniel Borkmann,
Kumar Kartikeya Dwivedi, Shuah Khan, bpf, linux-kselftest,
linux-kernel
Exercise recursive map dumping for array-of-maps and hash-of-maps,
including shared inner maps, empty outer and inner maps, BTF-formatted
values and multiple selected roots.
Check complete JSON documents against the existing nonrecursive entry
representations, and check plain headers and typed values. Verify both
short and long options, root ordering, deduplication and unchanged
default output.
All eight subtests pass in a matching-kernel VM.
Assisted-by: LLM
Signed-off-by: Tianyi Chen <hi@tychen.cc>
---
.../bpf/prog_tests/bpftool_map_dump.c | 252 ++++++++++++++++++
1 file changed, 252 insertions(+)
create mode 100644 tools/testing/selftests/bpf/prog_tests/bpftool_map_dump.c
diff --git a/tools/testing/selftests/bpf/prog_tests/bpftool_map_dump.c b/tools/testing/selftests/bpf/prog_tests/bpftool_map_dump.c
new file mode 100644
index 00000000000..3c425dbe2d9
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/bpftool_map_dump.c
@@ -0,0 +1,252 @@
+// SPDX-License-Identifier: GPL-2.0-only
+#include <test_progs.h>
+#include <bpftool_helpers.h>
+#include <bpf/btf.h>
+
+#define OUTPUT_SIZE 8192
+
+static bool dump_map(__u32 id, const char *options, char *output)
+{
+ char command[MAX_BPFTOOL_CMD_LEN];
+
+ snprintf(command, sizeof(command), "%s map dump id %u", options, id);
+ memset(output, 0, OUTPUT_SIZE);
+ if (!ASSERT_OK(get_bpftool_command_output(command, output, OUTPUT_SIZE - 1),
+ "dump_map"))
+ return false;
+ /* The helper doesn't terminate or strip the output. */
+ output[strcspn(output, "\n")] = '\0';
+ return true;
+}
+
+static __u32 map_id(int fd)
+{
+ struct bpf_map_info info = {};
+ __u32 len = sizeof(info);
+
+ if (!ASSERT_OK(bpf_map_get_info_by_fd(fd, &info, &len), "map_info"))
+ return 0;
+ return info.id;
+}
+
+static int count_token(const char *output, const char *token)
+{
+ int count = 0;
+
+ while ((output = strstr(output, token))) {
+ count++;
+ output += strlen(token);
+ }
+ return count;
+}
+
+static void check_plain(__u32 root_id, __u32 inner_id, const char *type,
+ int entries, bool typed)
+{
+ char command[MAX_BPFTOOL_CMD_LEN], header[128];
+ char output[OUTPUT_SIZE] = {};
+ const char *root, *inner;
+
+ snprintf(command, sizeof(command), "--recursive map dump id %u", root_id);
+ if (!ASSERT_OK(get_bpftool_command_output(command, output, sizeof(output) - 1),
+ "plain_dump"))
+ return;
+ snprintf(header, sizeof(header), "%u: %s name dump_outer ", root_id, type);
+ root = strstr(output, header);
+ if (!ASSERT_OK_PTR(root, "plain_root_header"))
+ return;
+ ASSERT_EQ(root - output, 0, "plain_root_first");
+ ASSERT_EQ(count_token(output, "inner_map_id:"), entries, "plain_references");
+ if (entries) {
+ snprintf(header, sizeof(header), "%u: hash name dump_inner ", inner_id);
+ inner = strstr(output, header);
+ if (ASSERT_OK_PTR(inner, "plain_inner_header"))
+ ASSERT_GT(inner - root, 0, "plain_inner_after_root");
+ ASSERT_EQ(count_token(output, header), 1, "plain_inner_once");
+ }
+ ASSERT_EQ(count_token(output, "Found "), entries && !typed ? 2 : 1,
+ "plain_map_count");
+ if (typed) {
+ ASSERT_HAS_SUBSTR(output, "\"key\": 0", "plain_btf_key");
+ ASSERT_HAS_SUBSTR(output, "\"value\": 16843009", "plain_btf_value");
+ }
+}
+
+static void test_outer(enum bpf_map_type type, int entries, bool empty_inner,
+ bool typed)
+{
+ LIBBPF_OPTS(bpf_map_create_opts, opts);
+ LIBBPF_OPTS(bpf_map_create_opts, inner_opts);
+ struct btf *btf = NULL;
+ char outer[OUTPUT_SIZE], inner[OUTPUT_SIZE], output[OUTPUT_SIZE];
+ char expected[OUTPUT_SIZE * 3], reference[64];
+ const char *type_name = libbpf_bpf_map_type_str(type);
+ int inner_fd = -1, outer_fd = -1;
+ __u32 root_id, inner_id, key, value = 0x01010101;
+
+ if (typed) {
+ btf = btf__new_empty();
+ if (!ASSERT_OK_PTR(btf, "create_btf") ||
+ !ASSERT_EQ(btf__add_int(btf, "unsigned int", 4, 0), 1, "btf_int") ||
+ !ASSERT_OK(btf__load_into_kernel(btf), "load_btf"))
+ goto out;
+ inner_opts.btf_fd = btf__fd(btf);
+ inner_opts.btf_key_type_id = 1;
+ inner_opts.btf_value_type_id = 1;
+ }
+ inner_fd = bpf_map_create(BPF_MAP_TYPE_HASH, "dump_inner", sizeof(key),
+ sizeof(value), 2, &inner_opts);
+ if (!ASSERT_OK_FD(inner_fd, "create_inner"))
+ goto out;
+ key = 0;
+ if (!empty_inner &&
+ !ASSERT_OK(bpf_map_update_elem(inner_fd, &key, &value, BPF_ANY),
+ "populate_inner"))
+ goto out;
+ opts.inner_map_fd = inner_fd;
+ outer_fd = bpf_map_create(type, "dump_outer", sizeof(key), sizeof(__u32),
+ 3, &opts);
+ if (!ASSERT_OK_FD(outer_fd, "create_outer"))
+ goto out;
+ /* The unused third array slot also exercises failed lookups. */
+ for (key = 0; key < entries; key++)
+ if (!ASSERT_OK(bpf_map_update_elem(outer_fd, &key, &inner_fd, BPF_ANY),
+ "populate_outer"))
+ goto out;
+ root_id = map_id(outer_fd);
+ inner_id = map_id(inner_fd);
+ if (!root_id || !inner_id || !dump_map(root_id, "-j", outer) ||
+ !dump_map(inner_id, "-j", inner))
+ goto out;
+
+ ASSERT_EQ(outer[0], '[', "default_array");
+ ASSERT_EQ(count_token(outer, "\"elements\":"), 0, "default_no_wrapper");
+ ASSERT_EQ(count_token(outer, "\"id\":"), 0, "default_no_header");
+ snprintf(reference, sizeof(reference), "\"inner_map_id\":%u", inner_id);
+ ASSERT_EQ(count_token(outer, reference), entries, "default_references");
+ if (!entries)
+ ASSERT_STREQ(outer, "[]", "empty_outer_default");
+ if (empty_inner)
+ ASSERT_STREQ(inner, "[]", "empty_inner_default");
+ else if (typed)
+ ASSERT_HAS_SUBSTR(inner, "\"formatted\":{\"key\":0,\"value\":16843009}",
+ "typed_inner");
+ else
+ ASSERT_STREQ(inner,
+ "[{\"key\":[\"0x00\",\"0x00\",\"0x00\",\"0x00\"],"
+ "\"value\":[\"0x01\",\"0x01\",\"0x01\",\"0x01\"]}]",
+ "ordinary_default");
+
+ /* Compare the complete JSON document: a flat array with the root first,
+ * one copy of the shared inner map, and unchanged entry representations.
+ */
+ if (entries)
+ snprintf(expected, sizeof(expected),
+ "[{\"id\":%u,\"type\":\"%s\",\"name\":\"dump_outer\","
+ "\"flags\":0,\"elements\":%s},{\"id\":%u,\"type\":\"hash\","
+ "\"name\":\"dump_inner\",\"flags\":0,\"elements\":%s}]",
+ root_id, type_name, outer, inner_id, inner);
+ else
+ snprintf(expected, sizeof(expected),
+ "[{\"id\":%u,\"type\":\"%s\",\"name\":\"dump_outer\","
+ "\"flags\":0,\"elements\":[]}]", root_id, type_name);
+ if (dump_map(root_id, "-j -r", output))
+ ASSERT_STREQ(output, expected, "recursive_json");
+ if (dump_map(root_id, "--json --recursive", output))
+ ASSERT_STREQ(output, expected, "recursive_long_options");
+ check_plain(root_id, inner_id, type_name, entries, typed);
+
+ /* Recursion on an ordinary map still emits a single map object. */
+ snprintf(expected, sizeof(expected),
+ "[{\"id\":%u,\"type\":\"hash\",\"name\":\"dump_inner\","
+ "\"flags\":0,\"elements\":%s}]", inner_id, inner);
+ if (dump_map(inner_id, "-j -r", output))
+ ASSERT_STREQ(output, expected, "ordinary_recursive");
+out:
+ if (outer_fd >= 0)
+ close(outer_fd);
+ if (inner_fd >= 0)
+ close(inner_fd);
+ btf__free(btf);
+}
+
+static void test_multiple_roots(void)
+{
+ LIBBPF_OPTS(bpf_map_create_opts, opts);
+ char command[MAX_BPFTOOL_CMD_LEN], name[BPF_OBJ_NAME_LEN];
+ char output[OUTPUT_SIZE] = {}, expected[OUTPUT_SIZE * 4], elements[OUTPUT_SIZE];
+ static const char * const types[] = { "hash", "array_of_maps", "hash_of_maps", "hash" };
+ int fds[] = { -1, -1, -1, -1 };
+ __u32 ids[4], key;
+ int i, len = 0;
+
+ /* Select the first inner map and both outers as roots. The other inner
+ * map must be appended after all three roots, even though it is found
+ * while dumping the first outer. A process-specific name avoids other
+ * tests' maps joining the selection.
+ */
+ snprintf(name, sizeof(name), "dump_%u", getpid());
+ fds[0] = bpf_map_create(BPF_MAP_TYPE_HASH, name, 4, 4, 1, NULL);
+ if (!ASSERT_OK_FD(fds[0], "create_selected_inner"))
+ goto out;
+ fds[3] = bpf_map_create(BPF_MAP_TYPE_HASH, "dump_discovered", 4, 4, 1, NULL);
+ if (!ASSERT_OK_FD(fds[3], "create_discovered_inner"))
+ goto out;
+ opts.inner_map_fd = fds[0];
+ fds[1] = bpf_map_create(BPF_MAP_TYPE_ARRAY_OF_MAPS, name, 4, 4, 2, &opts);
+ if (!ASSERT_OK_FD(fds[1], "create_array_root"))
+ goto out;
+ fds[2] = bpf_map_create(BPF_MAP_TYPE_HASH_OF_MAPS, name, 4, 4, 2, &opts);
+ if (!ASSERT_OK_FD(fds[2], "create_hash_root"))
+ goto out;
+ for (i = 1; i <= 2; i++) {
+ key = 0;
+ if (!ASSERT_OK(bpf_map_update_elem(fds[i], &key, &fds[0], BPF_ANY),
+ "reference_selected_inner"))
+ goto out;
+ key = 1;
+ if (!ASSERT_OK(bpf_map_update_elem(fds[i], &key, &fds[3], BPF_ANY),
+ "reference_discovered_inner"))
+ goto out;
+ }
+ for (i = 0; i < ARRAY_SIZE(fds); i++) {
+ ids[i] = map_id(fds[i]);
+ if (!ids[i] || !dump_map(ids[i], "-j", elements))
+ goto out;
+ len += snprintf(expected + len, sizeof(expected) - len,
+ "%s{\"id\":%u,\"type\":\"%s\",\"name\":\"%s\","
+ "\"flags\":0,\"elements\":%s}%s",
+ i ? "," : "[", ids[i], types[i],
+ i == 3 ? "dump_discovered" : name, elements, i == 3 ? "]" : "");
+ }
+ snprintf(command, sizeof(command), "-j -r map dump name %s", name);
+ if (ASSERT_OK(get_bpftool_command_output(command, output, sizeof(output) - 1),
+ "dump_multiple_roots")) {
+ output[strcspn(output, "\n")] = '\0';
+ ASSERT_STREQ(output, expected, "roots_first_and_seed_dedup");
+ }
+out:
+ for (i = 0; i < ARRAY_SIZE(fds); i++)
+ if (fds[i] >= 0)
+ close(fds[i]);
+}
+
+void test_bpftool_map_dump(void)
+{
+ if (test__start_subtest("multiple_roots"))
+ test_multiple_roots();
+ if (test__start_subtest("array_of_maps"))
+ test_outer(BPF_MAP_TYPE_ARRAY_OF_MAPS, 1, false, false);
+ if (test__start_subtest("hash_of_maps"))
+ test_outer(BPF_MAP_TYPE_HASH_OF_MAPS, 1, false, false);
+ if (test__start_subtest("shared_inner"))
+ test_outer(BPF_MAP_TYPE_ARRAY_OF_MAPS, 2, false, false);
+ if (test__start_subtest("empty_array_of_maps"))
+ test_outer(BPF_MAP_TYPE_ARRAY_OF_MAPS, 0, false, false);
+ if (test__start_subtest("empty_hash_of_maps"))
+ test_outer(BPF_MAP_TYPE_HASH_OF_MAPS, 0, false, false);
+ if (test__start_subtest("btf_inner"))
+ test_outer(BPF_MAP_TYPE_HASH_OF_MAPS, 1, false, true);
+ if (test__start_subtest("empty_inner"))
+ test_outer(BPF_MAP_TYPE_HASH_OF_MAPS, 1, true, false);
+}
--
2.55.0
^ permalink raw reply related [flat|nested] 4+ messages in thread* Re: [PATCH bpf-next 1/2] bpftool: Add recursive map dumping
2026-09-06 14:39 [PATCH bpf-next 1/2] bpftool: Add recursive map dumping Tianyi Chen
2026-09-06 14:39 ` [PATCH bpf-next 2/2] selftests/bpf: Cover recursive bpftool map dumps Tianyi Chen
@ 2026-09-06 15:29 ` bot+bpf-ci
2026-09-06 15:54 ` Tianyi Chen
1 sibling, 1 reply; 4+ messages in thread
From: bot+bpf-ci @ 2026-09-06 15:29 UTC (permalink / raw)
To: hi, qmo, andrii, eddyz87
Cc: hi, ast, daniel, memxor, shuah, bpf, linux-kselftest,
linux-kernel, ast, andrii, daniel, martin.lau, eddyz87,
yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 5255 bytes --]
> diff --git a/tools/bpf/bpftool/map.c b/tools/bpf/bpftool/map.c
> index 684a8fb72414..853f775b7d52 100644
> --- a/tools/bpf/bpftool/map.c
> +++ b/tools/bpf/bpftool/map.c
[ ... ]
> @@ -745,8 +745,10 @@ static int dump_map_elem(int fd, void *key, void *value,
> json_writer_t *btf_wtr)
> {
> if (bpf_map_lookup_elem(fd, key, value)) {
> - print_entry_error(map_info, key, errno);
> - return -1;
> + int lookup_errno = errno;
> +
> + print_entry_error(map_info, key, lookup_errno);
> + return -lookup_errno;
> }
>
> if (json_output) {
[ ... ]
> @@ -874,10 +928,22 @@ map_dump(int fd, struct bpf_map_info *info, json_writer_t *wtr,
> if (err) {
> if (errno == ENOENT)
> err = 0;
> + else if (ctx)
> + p_err("can't get next key for map id %u: %s",
> + info->id, strerror(errno));
> break;
> }
> - if (!dump_map_elem(fd, key, value, info, btf, wtr))
> + err = dump_map_elem(fd, key, value, info, btf, wtr);
> + if (!err) {
> num_elems++;
> + if (ctx && map_is_map_of_maps(info->type)) {
> + err = collect_inner_map(ctx, *(__u32 *)value);
> + if (err)
> + break;
> + }
> + } else if (ctx && err != -ENOENT) {
> + break;
> + }
> prev_key = key;
> }
Does the 'else if (ctx && err != -ENOENT) break;' path handle all
expected lookup failures correctly?
dump_map_elem() now returns -lookup_errno instead of -1, so any
bpf_map_lookup_elem() failure other than ENOENT aborts the recursive
dump. Several map types fail every syscall lookup by design:
* BPF_MAP_TYPE_PERF_EVENT_ARRAY / BPF_MAP_TYPE_CGROUP_ARRAY:
bpf_fd_array_map_lookup_elem() returns -ENOTSUPP when .map_fd_sys_lookup_elem
is not defined, and perf_event_array_map_ops does not define one.
* BPF_MAP_TYPE_XSKMAP: xsk_map_lookup_elem_sys_only() returns -EOPNOTSUPP
unconditionally.
* BPF_MAP_TYPE_REUSEPORT_SOCKARRAY with value_size != 8:
bpf_fd_reuseport_array_lookup_elem() returns -ENOSPC for every key.
print_entry_error() handles all of these, emitting '<cannot read>' or
strerror(lookup_errno), and the pre-patch loop ignored the result. So
'bpftool map dump id N' on such a map prints one error marker per entry
and exits 0.
With -r, 'bpftool map dump -r id N' on any of the map types above now
fails outright and exits nonzero where the same command without -r
succeeds. This also defeats the feature's own purpose: a PERF_EVENT_ARRAY
is a legal inner map, so recursing into a HASH_OF_MAPS/ARRAY_OF_MAPS of
PERF_EVENT_ARRAYs aborts with 'Unknown error 524' on the first inner-map
entry instead of dumping the traversal.
The new fatal handling looks intended for inner-map collection failures,
which is already handled by 'if (err) break;' on the collect_inner_map()
branch above. Could the per-entry value lookup failure keep the
pre-existing tolerant behaviour and continue rather than break?
> @@ -902,6 +970,7 @@ static int do_dump(int argc, char **argv)
> {
> json_writer_t *wtr = NULL, *btf_wtr = NULL;
> struct bpf_map_info info = {};
> + struct map_dump_ctx ctx = {};
> int nb_fds, i = 0;
> __u32 len = sizeof(info);
> int *fds = NULL;
[ ... ]
> @@ -936,7 +1032,7 @@ static int do_dump(int argc, char **argv)
> }
> }
>
> - if (wtr && nb_fds > 1)
> + if (wtr && (nb_fds > 1 || recursive))
> jsonw_start_array(wtr); /* root array */
> for (i = 0; i < nb_fds; i++) {
> if (bpf_map_get_info_by_fd(fds[i], &info, &len)) {
> @@ -944,22 +1040,28 @@ static int do_dump(int argc, char **argv)
> err = -1;
> break;
> }
> - err = map_dump(fds[i], &info, wtr, nb_fds > 1);
> + err = map_dump(fds[i], &info, wtr, nb_fds > 1 || recursive,
> + recursive ? &ctx : NULL);
> if (!wtr && i != nb_fds - 1)
> printf("\n");
>
> if (err)
> break;
> - close(fds[i]);
> + /* Keep discovered maps alive until the recursive dump is complete. */
> + if (!recursive)
> + close(fds[i]);
> }
Could the fd accumulation hit RLIMIT_NOFILE?
Under -r, every discovered inner map fd is held open until the entire
dump completes, so the number of simultaneously open fds scales with the
number of distinct maps reachable from the selected roots.
collect_inner_map() appends one fd per newly seen id and do_dump() skips
the in-loop close(), deferring all closes to exit_close. bpftool never
raises RLIMIT_NOFILE, so the inherited soft limit (commonly 1024) applies.
An ARRAY_OF_MAPS/HASH_OF_MAPS whose entries reference more distinct inner
maps than that limit makes bpf_map_get_fd_by_id_opts() fail with EMFILE,
producing 'can't open inner map id %u: Too many open files' and aborting
the dump after partial output has already been written.
max_entries for a map-of-maps is not bounded by the fd limit, so this is
reachable without any unusual privilege. Holding the fd does serve a
purpose - it pins the map so its id cannot be recycled between discovery
and dump. Could the number of concurrently held fds be bounded, or could
each discovered map be dumped as soon as it is found and closed then?
---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md
CI run summary: https://github.com/kernel-patches/bpf/actions/runs/34040347400
^ permalink raw reply [flat|nested] 4+ messages in thread