* [PATCH bpf-next v2 1/2] bpftool: Add recursive map dumping
@ 2026-09-06 15:53 Tianyi Chen
2026-09-06 15:53 ` [PATCH bpf-next v2 2/2] selftests/bpf: Cover recursive bpftool map dumps Tianyi Chen
` (2 more replies)
0 siblings, 3 replies; 11+ messages in thread
From: Tianyi Chen @ 2026-09-06 15:53 UTC (permalink / raw)
To: qmo, andrii, eddyz87
Cc: Tianyi Chen, ast, daniel, memxor, shuah, bpf, linux-kselftest,
linux-kernel, martin.lau, yonghong.song, mason, ihor.solodrai
Dumping a map-of-maps currently shows inner map IDs without their
contents. Add -r/--recursive to dump referenced inner maps as well,
leaving the default output unchanged.
Show the selected maps followed by discovered inner maps, with each
map's header and entries. JSON uses an array of these map objects.
Preserve BTF formatting in plain output, including when typed and
untyped maps are encountered in the same traversal.
Keep selected map FDs open and queue distinct inner map IDs. Open,
dump and close each queued map in turn, requiring only one additional
map FD regardless of the number of inner maps. Preserve per-entry
lookup error reporting for maps that do not support element lookups.
Report failure if an inner map cannot be opened and close any JSON
containers before returning. Document deferred ID resolution and the
non-atomic dump behavior, and add option completion.
Link: https://github.com/libbpf/bpftool/issues/58
Assisted-by: LLM
Signed-off-by: Tianyi Chen <hi@tychen.cc>
---
Changes in v2:
- Preserve per-entry lookup error handling for unsupported map types.
- Queue inner IDs and open/dump/close one inner map at a time.
- Document deferred ID resolution and non-atomic traversal.
Validation: all 12 bpftool_map_dump subtests pass in a matching-kernel
VM, with zero skips. The four new regression subtests fail with v1.
Injected ENOENT during inner-map FD lookup returns failure and valid JSON.
v1: https://lore.kernel.org/bpf/20260906143950.848769-1-hi@tychen.cc/
.../bpf/bpftool/Documentation/bpftool-map.rst | 21 ++-
tools/bpf/bpftool/bash-completion/bpftool | 2 +-
tools/bpf/bpftool/main.c | 7 +-
tools/bpf/bpftool/main.h | 1 +
tools/bpf/bpftool/map.c | 130 ++++++++++++++++--
5 files changed, 150 insertions(+), 11 deletions(-)
diff --git a/tools/bpf/bpftool/Documentation/bpftool-map.rst b/tools/bpf/bpftool/Documentation/bpftool-map.rst
index 5daf3de5c74..38d1f542bf5 100644
--- a/tools/bpf/bpftool/Documentation/bpftool-map.rst
+++ b/tools/bpf/bpftool/Documentation/bpftool-map.rst
@@ -16,7 +16,7 @@ SYNOPSIS
**bpftool** [*OPTIONS*] **map** *COMMAND*
-*OPTIONS* := { |COMMON_OPTIONS| | { **-f** | **--bpffs** } | { **-n** | **--nomount** } }
+*OPTIONS* := { |COMMON_OPTIONS| | { **-f** | **--bpffs** } | { **-n** | **--nomount** } | { **-r** | **--recursive** } }
*COMMANDS* :=
{ **show** | **list** | **create** | **dump** | **update** | **lookup** | **getnext** |
@@ -170,6 +170,25 @@ OPTIONS
Do not automatically attempt to mount any virtual file system (such as
tracefs or BPF virtual file system) when necessary.
+-r, --recursive
+ Also dump the inner maps referenced by **array_of_maps** and **hash_of_maps**
+ entries when running **map dump**. Each map ID is visited once, even if
+ several entries refer to it. Selected maps are followed by their inner maps.
+
+ Plain output includes a header identifying each map. On success, JSON output
+ is always an array of map objects, each containing an **id** and an
+ **elements** array, including when only one map is dumped. Outer map entries
+ retain their **inner_map_id** field, which identifies the corresponding inner
+ map object.
+
+ Inner map IDs are resolved when the maps are visited. The dump is not an
+ atomic snapshot: concurrent updates can change map contents or remove a
+ referenced inner map before it is visited. Failure to open a referenced
+ inner map stops the dump and returns a nonzero exit status. Output may
+ contain maps or entries printed before the error.
+ In JSON mode, an error during traversal is included in the output and the
+ enclosing arrays and objects are closed.
+
EXAMPLES
========
**# bpftool map show**
diff --git a/tools/bpf/bpftool/bash-completion/bpftool b/tools/bpf/bpftool/bash-completion/bpftool
index 75cbcb512eb..45c336d63be 100644
--- a/tools/bpf/bpftool/bash-completion/bpftool
+++ b/tools/bpf/bpftool/bash-completion/bpftool
@@ -261,7 +261,7 @@ _bpftool()
# Deal with options
if [[ ${words[cword]} == -* ]]; then
- local c='--version --json --pretty --bpffs --mapcompat --debug \
+ local c='--version --json --pretty --recursive --bpffs --mapcompat --debug \
--use-loader --base-btf --sign -i -k'
COMPREPLY=( $( compgen -W "$c" -- "$cur" ) )
return 0
diff --git a/tools/bpf/bpftool/main.c b/tools/bpf/bpftool/main.c
index c91e1a6e1a1..29021f4dd45 100644
--- a/tools/bpf/bpftool/main.c
+++ b/tools/bpf/bpftool/main.c
@@ -26,6 +26,7 @@ static int (*last_do_help)(int argc, char **argv);
json_writer_t *json_wtr;
bool pretty_output;
bool json_output;
+bool recursive;
bool show_pinned;
bool block_mount;
bool verifier_logs;
@@ -452,6 +453,7 @@ int main(int argc, char **argv)
{ "json", no_argument, NULL, 'j' },
{ "help", no_argument, NULL, 'h' },
{ "pretty", no_argument, NULL, 'p' },
+ { "recursive", no_argument, NULL, 'r' },
{ "version", no_argument, NULL, 'V' },
{ "bpffs", no_argument, NULL, 'f' },
{ "mapcompat", no_argument, NULL, 'm' },
@@ -485,7 +487,7 @@ int main(int argc, char **argv)
bin_name = "bpftool";
opterr = 0;
- while ((opt = getopt_long(argc, argv, "VhpjfLmndSi:k:B:l",
+ while ((opt = getopt_long(argc, argv, "VhpjrfLmndSi:k:B:l",
options, NULL)) >= 0) {
switch (opt) {
case 'V':
@@ -507,6 +509,9 @@ int main(int argc, char **argv)
}
jsonw_pretty(json_wtr, pretty_output);
break;
+ case 'r':
+ recursive = true;
+ break;
case 'f':
show_pinned = true;
break;
diff --git a/tools/bpf/bpftool/main.h b/tools/bpf/bpftool/main.h
index 78b6e0ebb85..b5dc7960cf6 100644
--- a/tools/bpf/bpftool/main.h
+++ b/tools/bpf/bpftool/main.h
@@ -83,6 +83,7 @@ extern const char *bin_name;
extern json_writer_t *json_wtr;
extern bool json_output;
+extern bool recursive;
extern bool show_pinned;
extern bool show_pids;
extern bool block_mount;
diff --git a/tools/bpf/bpftool/map.c b/tools/bpf/bpftool/map.c
index 684a8fb7241..fc06c765973 100644
--- a/tools/bpf/bpftool/map.c
+++ b/tools/bpf/bpftool/map.c
@@ -17,6 +17,7 @@
#include <bpf/bpf.h>
#include <bpf/btf.h>
#include <bpf/hashmap.h>
+#include <bpf/libbpf_internal.h>
#include "json_writer.h"
#include "main.h"
@@ -826,12 +827,44 @@ static void free_map_kv_btf(struct btf *btf)
btf__free(btf);
}
+struct map_dump_ctx {
+ struct hashmap *seen;
+ __u32 *pending_ids;
+ size_t pending_cnt;
+};
+
+static int collect_inner_map(struct map_dump_ctx *ctx, __u32 id)
+{
+ __u32 *ids;
+ int err;
+
+ if (hashmap__find(ctx->seen, id, NULL))
+ return 0;
+
+ ids = libbpf_reallocarray(ctx->pending_ids, ctx->pending_cnt + 1,
+ sizeof(*ids));
+ if (!ids) {
+ p_err("mem alloc failed");
+ return -1;
+ }
+ ctx->pending_ids = ids;
+
+ err = hashmap__add(ctx->seen, id, 0);
+ if (err) {
+ p_err("failed to record inner map id %u: %s", id, strerror(-err));
+ return -1;
+ }
+ ids[ctx->pending_cnt++] = id;
+ return 0;
+}
+
static int
map_dump(int fd, struct bpf_map_info *info, json_writer_t *wtr,
- bool show_header)
+ bool show_header, struct map_dump_ctx *ctx)
{
void *key, *value, *prev_key;
unsigned int num_elems = 0;
+ json_writer_t *plain_btf_wtr = NULL;
struct btf *btf = NULL;
int err;
@@ -845,6 +878,17 @@ map_dump(int fd, struct bpf_map_info *info, json_writer_t *wtr,
prev_key = NULL;
+ if (ctx && !wtr && (info->btf_value_type_id ||
+ info->btf_vmlinux_value_type_id)) {
+ plain_btf_wtr = get_btf_writer();
+ if (plain_btf_wtr) {
+ if (show_header)
+ show_map_header_plain(info);
+ show_header = false;
+ wtr = plain_btf_wtr;
+ }
+ }
+
if (wtr) {
err = get_map_kv_btf(info, &btf);
if (err) {
@@ -874,10 +918,20 @@ 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;
+ }
+ }
prev_key = key;
}
@@ -894,18 +948,25 @@ map_dump(int fd, struct bpf_map_info *info, json_writer_t *wtr,
free(key);
free(value);
free_map_kv_btf(btf);
+ if (plain_btf_wtr)
+ jsonw_destroy(&plain_btf_wtr);
return err;
}
static int do_dump(int argc, char **argv)
{
+ LIBBPF_OPTS(bpf_get_fd_by_id_opts, opts,
+ .open_flags = BPF_F_RDONLY,
+ );
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;
int err = -1;
+ size_t j;
if (argc != 2)
usage();
@@ -919,9 +980,34 @@ static int do_dump(int argc, char **argv)
if (nb_fds < 1)
goto exit_free;
+ if (recursive) {
+ ctx.seen = hashmap__new(hash_fn_for_key_as_id,
+ equal_fn_for_key_as_id, NULL);
+ if (IS_ERR(ctx.seen)) {
+ ctx.seen = NULL;
+ p_err("failed to create hashmap for recursive dump");
+ goto exit_close;
+ }
+ /* Record the selected maps before discovering any inner maps. */
+ for (i = 0; i < nb_fds; i++) {
+ len = sizeof(info);
+ if (bpf_map_get_info_by_fd(fds[i], &info, &len)) {
+ p_err("can't get map info: %s", strerror(errno));
+ err = -1;
+ goto exit_close;
+ }
+ err = hashmap__add(ctx.seen, info.id, 0);
+ if (err) {
+ p_err("failed to record map id %u: %s", info.id,
+ strerror(-err));
+ goto exit_close;
+ }
+ }
+ }
+
if (json_output) {
wtr = json_wtr;
- } else {
+ } else if (!recursive) {
int do_plain_btf;
do_plain_btf = maps_have_btf(fds, nb_fds);
@@ -936,7 +1022,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 +1030,50 @@ 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 selected maps alive while visiting their inner maps. */
+ if (!recursive)
+ close(fds[i]);
+ }
+ for (j = 0; !err && j < ctx.pending_cnt; j++) {
+ int fd;
+
+ fd = bpf_map_get_fd_by_id_opts(ctx.pending_ids[j], &opts);
+ if (fd < 0) {
+ p_err("can't open inner map id %u: %s",
+ ctx.pending_ids[j], strerror(errno));
+ err = -1;
+ break;
+ }
+ len = sizeof(info);
+ if (bpf_map_get_info_by_fd(fd, &info, &len)) {
+ p_err("can't get map info: %s", strerror(errno));
+ err = -1;
+ } else {
+ if (!wtr)
+ printf("\n");
+ err = map_dump(fd, &info, wtr, true, &ctx);
+ }
+ close(fd);
}
- if (wtr && nb_fds > 1)
+ if (wtr && (nb_fds > 1 || recursive))
jsonw_end_array(wtr); /* root array */
if (btf_wtr)
jsonw_destroy(&btf_wtr);
exit_close:
+ if (recursive)
+ i = 0;
for (; i < nb_fds; i++)
close(fds[i]);
+ hashmap__free(ctx.seen);
+ free(ctx.pending_ids);
exit_free:
free(fds);
free_btf_vmlinux();
@@ -1484,7 +1598,7 @@ static int do_help(int argc, char **argv)
" task_storage | bloom_filter | user_ringbuf | cgrp_storage | arena |\n"
" insn_array | rhash }\n"
" " HELP_SPEC_OPTIONS " |\n"
- " {-f|--bpffs} | {-n|--nomount} }\n"
+ " {-f|--bpffs} | {-n|--nomount} | {-r|--recursive} }\n"
"",
bin_name, argv[-2]);
--
2.55.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH bpf-next v2 2/2] selftests/bpf: Cover recursive bpftool map dumps
2026-09-06 15:53 [PATCH bpf-next v2 1/2] bpftool: Add recursive map dumping Tianyi Chen
@ 2026-09-06 15:53 ` Tianyi Chen
2026-09-06 16:18 ` sashiko-bot
2026-09-06 17:02 ` [PATCH bpf-next v2 1/2] bpftool: Add recursive map dumping bot+bpf-ci
2026-09-07 1:21 ` [PATCH bpf-next v3 0/2] " Tianyi Chen
2 siblings, 1 reply; 11+ messages in thread
From: Tianyi Chen @ 2026-09-06 15:53 UTC (permalink / raw)
To: qmo, andrii, eddyz87
Cc: Tianyi Chen, ast, daniel, memxor, shuah, bpf, linux-kselftest,
linux-kernel, martin.lau, yonghong.song, mason, ihor.solodrai
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.
Cover ordinary and nested perf event arrays to ensure unsupported
element lookups retain their error markers without failing the dump.
Dump 64 distinct inner maps under RLIMIT_NOFILE=32 in both JSON and
plain modes to catch holding every discovered inner-map FD open.
Assisted-by: LLM
Signed-off-by: Tianyi Chen <hi@tychen.cc>
---
Changes in v2:
- Add ordinary and nested PERF_EVENT_ARRAY lookup-error coverage.
- Add JSON and plain dumps of 64 inner maps with RLIMIT_NOFILE=32.
Validation: all 12 bpftool_map_dump subtests pass in a matching-kernel
VM, with zero skips. The four new regression subtests fail with v1.
Injected ENOENT during inner-map FD lookup returns failure and valid JSON.
v1: https://lore.kernel.org/bpf/20260906143950.848769-1-hi@tychen.cc/
.../bpf/prog_tests/bpftool_map_dump.c | 419 ++++++++++++++++++
1 file changed, 419 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..330a3344770
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/bpftool_map_dump.c
@@ -0,0 +1,419 @@
+// SPDX-License-Identifier: GPL-2.0-only
+#include <test_progs.h>
+#include <bpftool_helpers.h>
+#include <bpf/btf.h>
+#include <sys/resource.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]);
+}
+
+static void test_unreadable(bool outer)
+{
+ LIBBPF_OPTS(bpf_map_create_opts, opts);
+ char elements[OUTPUT_SIZE], output[OUTPUT_SIZE], root[OUTPUT_SIZE];
+ char expected[OUTPUT_SIZE * 3], plain[OUTPUT_SIZE] = {};
+ char command[MAX_BPFTOOL_CMD_LEN];
+ int inner_fd = -1, outer_fd = -1, lookup_errno;
+ __u32 inner_id, root_id, key = 0, value;
+
+ /* Every key is enumerable, but PERF_EVENT_ARRAY lookup returns
+ * ENOTSUPP (the kernel-internal errno). Check both entries so an
+ * early exit on the first lookup failure cannot pass.
+ */
+ inner_fd = bpf_map_create(BPF_MAP_TYPE_PERF_EVENT_ARRAY, "dump_unreadable",
+ sizeof(key), sizeof(value), 2, NULL);
+ if (!ASSERT_OK_FD(inner_fd, "create_unreadable"))
+ goto out;
+ if (!ASSERT_LT(bpf_map_lookup_elem(inner_fd, &key, &value), 0,
+ "unreadable_lookup"))
+ goto out;
+ lookup_errno = errno;
+ if (!ASSERT_NEQ(lookup_errno, ENOENT, "unreadable_not_missing"))
+ goto out;
+ inner_id = map_id(inner_fd);
+ if (!inner_id || !dump_map(inner_id, "-j", elements))
+ goto out;
+ ASSERT_EQ(count_token(elements, "\"error\":"), 2, "default_json_errors");
+ snprintf(command, sizeof(command), "map dump id %u", inner_id);
+ if (!ASSERT_OK(get_bpftool_command_output(command, plain, sizeof(plain) - 1),
+ "default_plain_unreadable"))
+ goto out;
+ ASSERT_EQ(count_token(plain, strerror(lookup_errno)), 2, "default_plain_errors");
+ ASSERT_HAS_SUBSTR(plain, "Found 0 elements", "default_plain_count");
+
+ root_id = inner_id;
+ if (outer) {
+ opts.inner_map_fd = inner_fd;
+ outer_fd = bpf_map_create(BPF_MAP_TYPE_ARRAY_OF_MAPS, "dump_outer",
+ sizeof(key), sizeof(value), 1, &opts);
+ if (!ASSERT_OK_FD(outer_fd, "create_outer") ||
+ !ASSERT_OK(bpf_map_update_elem(outer_fd, &key, &inner_fd, BPF_ANY),
+ "populate_outer"))
+ goto out;
+ root_id = map_id(outer_fd);
+ if (!root_id || !dump_map(root_id, "-j", root))
+ goto out;
+ snprintf(expected, sizeof(expected),
+ "[{\"id\":%u,\"type\":\"array_of_maps\",\"name\":\"dump_outer\","
+ "\"flags\":0,\"elements\":%s},{\"id\":%u,"
+ "\"type\":\"perf_event_array\",\"name\":\"dump_unreadable\","
+ "\"flags\":0,\"elements\":%s}]", root_id, root, inner_id, elements);
+ } else {
+ snprintf(expected, sizeof(expected),
+ "[{\"id\":%u,\"type\":\"perf_event_array\","
+ "\"name\":\"dump_unreadable\",\"flags\":0,\"elements\":%s}]",
+ inner_id, elements);
+ }
+ if (dump_map(root_id, "-j -r", output))
+ ASSERT_STREQ(output, expected, "recursive_unreadable_json");
+ memset(output, 0, sizeof(output));
+ snprintf(command, sizeof(command), "-r map dump id %u", root_id);
+ if (ASSERT_OK(get_bpftool_command_output(command, output, sizeof(output) - 1),
+ "recursive_unreadable_plain")) {
+ ASSERT_HAS_SUBSTR(output, plain, "recursive_plain_preserves_errors");
+ ASSERT_EQ(count_token(output, strerror(lookup_errno)), 2,
+ "recursive_plain_errors");
+ ASSERT_EQ(count_token(output, "Found "), outer ? 2 : 1,
+ "recursive_plain_maps");
+ }
+out:
+ if (outer_fd >= 0)
+ close(outer_fd);
+ if (inner_fd >= 0)
+ close(inner_fd);
+}
+
+static void test_many_inner_maps(bool json)
+{
+ LIBBPF_OPTS(bpf_map_create_opts, opts);
+ const struct rlimit limit = { .rlim_cur = 32, .rlim_max = 32 };
+ char command[MAX_BPFTOOL_CMD_LEN], token[64];
+ __u32 ids[64], root_id, key;
+ int inner_fd = -1, outer_fd = -1, status;
+ char *output = NULL;
+ pid_t pid;
+
+ inner_fd = bpf_map_create(BPF_MAP_TYPE_HASH, "dump_inner", 4, 4, 1, NULL);
+ if (!ASSERT_OK_FD(inner_fd, "create_template"))
+ goto out;
+ opts.inner_map_fd = inner_fd;
+ outer_fd = bpf_map_create(BPF_MAP_TYPE_ARRAY_OF_MAPS, "dump_outer", 4, 4,
+ ARRAY_SIZE(ids), &opts);
+ close(inner_fd);
+ inner_fd = -1;
+ if (!ASSERT_OK_FD(outer_fd, "create_outer"))
+ goto out;
+ for (key = 0; key < ARRAY_SIZE(ids); key++) {
+ inner_fd = bpf_map_create(BPF_MAP_TYPE_HASH, "dump_inner", 4, 4, 1, NULL);
+ if (!ASSERT_OK_FD(inner_fd, "create_inner") ||
+ !ASSERT_OK(bpf_map_update_elem(outer_fd, &key, &inner_fd, BPF_ANY),
+ "populate_outer"))
+ goto out;
+ ids[key] = map_id(inner_fd);
+ if (!ids[key])
+ goto out;
+ /* The outer map keeps each distinct inner map alive. */
+ close(inner_fd);
+ inner_fd = -1;
+ }
+ root_id = map_id(outer_fd);
+ output = calloc(1, 65536);
+ if (!root_id || !ASSERT_OK_PTR(output, "allocate_output"))
+ goto out;
+
+ /* Create all fixtures before lowering the limit, and keep the test
+ * runner's limit unchanged. Retaining every discovered FD would exceed
+ * this limit before the recursive dump could visit all inner maps.
+ */
+ pid = fork();
+ if (!ASSERT_GE(pid, 0, "fork"))
+ goto out;
+ if (!pid) {
+ close(outer_fd);
+ if (setrlimit(RLIMIT_NOFILE, &limit))
+ _exit(1);
+ snprintf(command, sizeof(command), "%s -r map dump id %u",
+ json ? "-j" : "", root_id);
+ if (get_bpftool_command_output(command, output, 65535))
+ _exit(2);
+ if (count_token(output, json ? "\"id\":" : "Found ") != ARRAY_SIZE(ids) + 1 ||
+ count_token(output, json ? "\"inner_map_id\":" : "inner_map_id:") !=
+ ARRAY_SIZE(ids))
+ _exit(3);
+ for (key = 0; key < ARRAY_SIZE(ids); key++) {
+ if (json)
+ snprintf(token, sizeof(token), "\"id\":%u,", ids[key]);
+ else
+ snprintf(token, sizeof(token), "\n%u: hash name dump_inner ",
+ ids[key]);
+ if (count_token(output, token) != 1)
+ _exit(4);
+ }
+ if (json && (output[0] != '[' ||
+ strcmp(output + strlen(output) - 2, "]\n")))
+ _exit(5);
+ _exit(0);
+ }
+ if (ASSERT_EQ(waitpid(pid, &status, 0), pid, "waitpid") &&
+ ASSERT_TRUE(WIFEXITED(status), "child_exited"))
+ ASSERT_EQ(WEXITSTATUS(status), 0, "dump_with_low_fd_limit");
+out:
+ free(output);
+ if (outer_fd >= 0)
+ close(outer_fd);
+ if (inner_fd >= 0)
+ close(inner_fd);
+}
+
+void test_bpftool_map_dump(void)
+{
+ if (test__start_subtest("unreadable_ordinary"))
+ test_unreadable(false);
+ if (test__start_subtest("unreadable_inner"))
+ test_unreadable(true);
+ if (test__start_subtest("many_inner_maps_json"))
+ test_many_inner_maps(true);
+ if (test__start_subtest("many_inner_maps_plain"))
+ test_many_inner_maps(false);
+ 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] 11+ messages in thread
* Re: [PATCH bpf-next v2 2/2] selftests/bpf: Cover recursive bpftool map dumps
2026-09-06 15:53 ` [PATCH bpf-next v2 2/2] selftests/bpf: Cover recursive bpftool map dumps Tianyi Chen
@ 2026-09-06 16:18 ` sashiko-bot
0 siblings, 0 replies; 11+ messages in thread
From: sashiko-bot @ 2026-09-06 16:18 UTC (permalink / raw)
To: Tianyi Chen; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] Lowering RLIMIT_NOFILE without closing inherited FDs from the test runner creates brittle test execution.
- [Low] Multi-line comments do not follow the BPF subsystem standard (opening `/*` on its own line).
--
commit bfea95162d84a988e16f2cd61621377edfd83849
Author: Tianyi Chen <hi@tychen.cc>
selftests/bpf: Cover recursive bpftool map dumps
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.
> diff --git a/tools/testing/selftests/bpf/prog_tests/bpftool_map_dump.c b/tools/testing/selftests/bpf/prog_tests/bpftool_map_dump.c
> index 0000000000000..330a3344770d1
> --- /dev/null
> +++ b/tools/testing/selftests/bpf/prog_tests/bpftool_map_dump.c
> @@ -0,0 +1,419 @@
[ ... ]
> + /* Compare the complete JSON document: a flat array with the root first,
> + * one copy of the shared inner map, and unchanged entry representations.
> + */
[Severity: Low]
Does this follow the BPF subsystem multi-line comment style? The opening
/* should be on its own line:
/*
* Compare the complete JSON document...
[ ... ]
> + /* 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.
> + */
[Severity: Low]
Does this follow the BPF subsystem multi-line comment style? The opening
/* should be on its own line.
[ ... ]
> + /* Every key is enumerable, but PERF_EVENT_ARRAY lookup returns
> + * ENOTSUPP (the kernel-internal errno). Check both entries so an
> + * early exit on the first lookup failure cannot pass.
> + */
[Severity: Low]
Does this follow the BPF subsystem multi-line comment style? The opening
/* should be on its own line.
[ ... ]
> + /* Create all fixtures before lowering the limit, and keep the test
> + * runner's limit unchanged. Retaining every discovered FD would exceed
> + * this limit before the recursive dump could visit all inner maps.
> + */
[Severity: Low]
Does this follow the BPF subsystem multi-line comment style? The opening
/* should be on its own line.
> + pid = fork();
> + if (!ASSERT_GE(pid, 0, "fork"))
> + goto out;
> + if (!pid) {
> + close(outer_fd);
> + if (setrlimit(RLIMIT_NOFILE, &limit))
> + _exit(1);
> + snprintf(command, sizeof(command), "%s -r map dump id %u",
> + json ? "-j" : "", root_id);
> + if (get_bpftool_command_output(command, output, 65535))
> + _exit(2);
[Severity: Medium]
Will this sequence cause sporadic test failures if the parent test runner
already has 32 or more file descriptors open?
Since the child process inherits all file descriptors from the parent,
lowering RLIMIT_NOFILE to 32 here leaves no available file descriptor
numbers if the parent already holds >= 32 descriptors (which is common
when running the full test suite).
Later in the block, get_bpftool_command_output() relies on popen(), which
needs to allocate new descriptors for a pipe. If no descriptors are
available, opening the pipe will fail with EMFILE, aborting the test
prematurely.
Should the child process explicitly close all inherited file descriptors
(e.g., using close_range(3, ~0U, 0)) before calling setrlimit?
--
Sashiko AI review · https://sashiko.dev/#/patchset/178870998583.979169.16526173782819097354@tychen.cc?part=2
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH bpf-next v2 1/2] bpftool: Add recursive map dumping
2026-09-06 15:53 [PATCH bpf-next v2 1/2] bpftool: Add recursive map dumping Tianyi Chen
2026-09-06 15:53 ` [PATCH bpf-next v2 2/2] selftests/bpf: Cover recursive bpftool map dumps Tianyi Chen
@ 2026-09-06 17:02 ` bot+bpf-ci
2026-09-07 1:21 ` [PATCH bpf-next v3 0/2] " Tianyi Chen
2 siblings, 0 replies; 11+ messages in thread
From: bot+bpf-ci @ 2026-09-06 17:02 UTC (permalink / raw)
To: hi, qmo, andrii, eddyz87
Cc: hi, ast, daniel, memxor, shuah, bpf, linux-kselftest,
linux-kernel, martin.lau, yonghong.song, mason, ihor.solodrai,
ast, andrii, daniel, martin.lau, eddyz87, yonghong.song, mason,
ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 3164 bytes --]
> diff --git a/tools/bpf/bpftool/Documentation/bpftool-map.rst b/tools/bpf/bpftool/Documentation/bpftool-map.rst
> index 5daf3de5c744..38d1f542bf5a 100644
> --- a/tools/bpf/bpftool/Documentation/bpftool-map.rst
> +++ b/tools/bpf/bpftool/Documentation/bpftool-map.rst
[ ... ]
> @@ -170,6 +170,25 @@ OPTIONS
> Do not automatically attempt to mount any virtual file system (such as
> tracefs or BPF virtual file system) when necessary.
>
> +-r, --recursive
> + Also dump the inner maps referenced by **array_of_maps** and **hash_of_maps**
> + entries when running **map dump**. Each map ID is visited once, even if
> + several entries refer to it. Selected maps are followed by their inner maps.
> +
> + Plain output includes a header identifying each map. On success, JSON output
> + is always an array of map objects, each containing an **id** and an
> + **elements** array, including when only one map is dumped. Outer map entries
> + retain their **inner_map_id** field, which identifies the corresponding inner
> + map object.
> +
> + Inner map IDs are resolved when the maps are visited. The dump is not an
> + atomic snapshot: concurrent updates can change map contents or remove a
> + referenced inner map before it is visited. Failure to open a referenced
> + inner map stops the dump and returns a nonzero exit status. Output may
> + contain maps or entries printed before the error.
> + In JSON mode, an error during traversal is included in the output and the
> + enclosing arrays and objects are closed.
This isn't a bug, but would it be worth reflowing this option block
to 80 columns? Two lines run to 81 columns (the "Also dump the inner
maps referenced by" line and the "retain their **inner_map_id** field"
line), making them the only prose lines over 80 columns in the file.
Also, should there be a blank line before "In JSON mode, ..." if it's
meant to be its own paragraph? It currently starts on a fresh line
after a short 53-column line, so it renders as a continuation of the
same paragraph rather than separated like the other paragraphs in this
block.
[ ... ]
> diff --git a/tools/bpf/bpftool/main.c b/tools/bpf/bpftool/main.c
> index 5ababd8f7d0a..33f4c7172583 100644
> --- a/tools/bpf/bpftool/main.c
> +++ b/tools/bpf/bpftool/main.c
> @@ -26,6 +26,7 @@ static int (*last_do_help)(int argc, char **argv);
> json_writer_t *json_wtr;
> bool pretty_output;
> bool json_output;
> +bool recursive;
> bool show_pinned;
> bool block_mount;
> bool verifier_logs;
This isn't a bug, but would a name like recursive_dump or dump_recursive
read better here? It's a program-wide global exported in main.h, and the
neighbouring flags all carry a qualifier that says what they apply to:
json_output, pretty_output, show_pinned, relaxed_maps, verifier_logs.
Note that --json and --pretty didn't become json or pretty for the same
reason.
[ ... ]
---
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/34044964295
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH bpf-next v3 0/2] bpftool: Add recursive map dumping
2026-09-06 15:53 [PATCH bpf-next v2 1/2] bpftool: Add recursive map dumping Tianyi Chen
2026-09-06 15:53 ` [PATCH bpf-next v2 2/2] selftests/bpf: Cover recursive bpftool map dumps Tianyi Chen
2026-09-06 17:02 ` [PATCH bpf-next v2 1/2] bpftool: Add recursive map dumping bot+bpf-ci
@ 2026-09-07 1:21 ` Tianyi Chen
2026-09-07 1:21 ` [PATCH bpf-next v3 1/2] " Tianyi Chen
2026-09-07 1:21 ` [PATCH bpf-next v3 2/2] selftests/bpf: Cover recursive bpftool map dumps Tianyi Chen
2 siblings, 2 replies; 11+ messages in thread
From: Tianyi Chen @ 2026-09-07 1:21 UTC (permalink / raw)
To: bpf; +Cc: Tianyi Chen, Quentin Monnet, linux-kselftest
Add recursive dumping for maps of maps, including shared inner maps,
BTF formatting, unchanged default output and bounded descriptor usage.
Changes in v3:
- Close inherited descriptors in the low-FD-limit child before setting
RLIMIT_NOFILE=32. Use /proc/self/fd rather than requiring close_range
syscall definitions or support from the running kernel.
- Open 32 extra descriptors before fork to cover the reported failure;
verify that the parent's descriptors and outer map remain usable.
- Rename the global option to recursive_dump, reflow the option text,
separate the JSON paragraph and fix multiline comment openings.
- Document JSON container closure without promising that every kind of
traversal diagnostic is included in stdout.
- Use the required tool:model Assisted-by attribution format.
Validation:
- Full bpftool and focused BPF selftest builds with LLVM 20, plus man page
generation, succeeded.
- All 69 focused subtests passed with the flags, ring-buffer and batch
v2 series in an x86-64 KVM guest running Linux 7.3.0-rc1.
- After the final descriptor-cleanup adjustment, all 12 recursive dump
subtests passed again, including both low-FD-limit cases.
- No skips or failures in these focused runs; the full BPF selftest
suite was not run.
v2: https://lore.kernel.org/r/178870998583.979169.16526173782819097354@tychen.cc
Request: https://github.com/libbpf/bpftool/issues/58
Tianyi Chen (2):
bpftool: Add recursive map dumping
selftests/bpf: Cover recursive bpftool map dumps
.../bpf/bpftool/Documentation/bpftool-map.rst | 22 +-
tools/bpf/bpftool/bash-completion/bpftool | 2 +-
tools/bpf/bpftool/main.c | 7 +-
tools/bpf/bpftool/main.h | 1 +
tools/bpf/bpftool/map.c | 130 ++++-
.../bpf/prog_tests/bpftool_map_dump.c | 465 ++++++++++++++++++
6 files changed, 616 insertions(+), 11 deletions(-)
create mode 100644 tools/testing/selftests/bpf/prog_tests/bpftool_map_dump.c
--
2.55.0
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH bpf-next v3 1/2] bpftool: Add recursive map dumping
2026-09-07 1:21 ` [PATCH bpf-next v3 0/2] " Tianyi Chen
@ 2026-09-07 1:21 ` Tianyi Chen
2026-09-07 2:21 ` bot+bpf-ci
2026-09-07 1:21 ` [PATCH bpf-next v3 2/2] selftests/bpf: Cover recursive bpftool map dumps Tianyi Chen
1 sibling, 1 reply; 11+ messages in thread
From: Tianyi Chen @ 2026-09-07 1:21 UTC (permalink / raw)
To: bpf; +Cc: Tianyi Chen, Quentin Monnet, linux-kselftest
Dumping a map-of-maps currently shows inner map IDs without their
contents. Add -r/--recursive to dump referenced inner maps as well,
leaving the default output unchanged.
Show the selected maps followed by discovered inner maps, with each
map's header and entries. JSON uses an array of these map objects.
Preserve BTF formatting in plain output, including when typed and
untyped maps are encountered in the same traversal.
Keep selected map FDs open and queue distinct inner map IDs. Open,
dump and close each queued map in turn, requiring only one additional
map FD regardless of the number of inner maps. Preserve per-entry
lookup error reporting for maps that do not support element lookups.
Report failure if an inner map cannot be opened and close any JSON
containers before returning. Document deferred ID resolution and the
non-atomic dump behavior, and add option completion.
Link: https://github.com/libbpf/bpftool/issues/58
Assisted-by: Codex:GPT-6
Signed-off-by: Tianyi Chen <hi@tychen.cc>
---
.../bpf/bpftool/Documentation/bpftool-map.rst | 22 ++-
tools/bpf/bpftool/bash-completion/bpftool | 2 +-
tools/bpf/bpftool/main.c | 7 +-
tools/bpf/bpftool/main.h | 1 +
tools/bpf/bpftool/map.c | 130 ++++++++++++++++--
5 files changed, 151 insertions(+), 11 deletions(-)
diff --git a/tools/bpf/bpftool/Documentation/bpftool-map.rst b/tools/bpf/bpftool/Documentation/bpftool-map.rst
index 5daf3de5c74..23faa672685 100644
--- a/tools/bpf/bpftool/Documentation/bpftool-map.rst
+++ b/tools/bpf/bpftool/Documentation/bpftool-map.rst
@@ -16,7 +16,8 @@ SYNOPSIS
**bpftool** [*OPTIONS*] **map** *COMMAND*
-*OPTIONS* := { |COMMON_OPTIONS| | { **-f** | **--bpffs** } | { **-n** | **--nomount** } }
+*OPTIONS* := { |COMMON_OPTIONS| | { **-f** | **--bpffs** } |
+{ **-n** | **--nomount** } | { **-r** | **--recursive** } }
*COMMANDS* :=
{ **show** | **list** | **create** | **dump** | **update** | **lookup** | **getnext** |
@@ -170,6 +171,25 @@ OPTIONS
Do not automatically attempt to mount any virtual file system (such as
tracefs or BPF virtual file system) when necessary.
+-r, --recursive
+ Also dump the inner maps referenced by **array_of_maps** and **hash_of_maps**
+ entries when running **map dump**. Each map ID is visited once, even if
+ several entries refer to it. Selected maps are followed by their inner maps.
+
+ Plain output includes a header identifying each map. On success, JSON output
+ is always an array of map objects, each containing an **id** and an
+ **elements** array, including when only one map is dumped. Outer map entries
+ retain their **inner_map_id** field, which identifies the corresponding inner
+ map object.
+
+ Inner map IDs are resolved when the maps are visited. The dump is not an
+ atomic snapshot: concurrent updates can change map contents or remove a
+ referenced inner map before it is visited. Failure to open a referenced
+ inner map stops the dump and returns a nonzero exit status. Output may
+ contain maps or entries printed before the error.
+
+ In JSON mode, the enclosing arrays and objects are closed before returning.
+
EXAMPLES
========
**# bpftool map show**
diff --git a/tools/bpf/bpftool/bash-completion/bpftool b/tools/bpf/bpftool/bash-completion/bpftool
index 75cbcb512eb..45c336d63be 100644
--- a/tools/bpf/bpftool/bash-completion/bpftool
+++ b/tools/bpf/bpftool/bash-completion/bpftool
@@ -261,7 +261,7 @@ _bpftool()
# Deal with options
if [[ ${words[cword]} == -* ]]; then
- local c='--version --json --pretty --bpffs --mapcompat --debug \
+ local c='--version --json --pretty --recursive --bpffs --mapcompat --debug \
--use-loader --base-btf --sign -i -k'
COMPREPLY=( $( compgen -W "$c" -- "$cur" ) )
return 0
diff --git a/tools/bpf/bpftool/main.c b/tools/bpf/bpftool/main.c
index c91e1a6e1a1..424af4a496a 100644
--- a/tools/bpf/bpftool/main.c
+++ b/tools/bpf/bpftool/main.c
@@ -26,6 +26,7 @@ static int (*last_do_help)(int argc, char **argv);
json_writer_t *json_wtr;
bool pretty_output;
bool json_output;
+bool recursive_dump;
bool show_pinned;
bool block_mount;
bool verifier_logs;
@@ -452,6 +453,7 @@ int main(int argc, char **argv)
{ "json", no_argument, NULL, 'j' },
{ "help", no_argument, NULL, 'h' },
{ "pretty", no_argument, NULL, 'p' },
+ { "recursive", no_argument, NULL, 'r' },
{ "version", no_argument, NULL, 'V' },
{ "bpffs", no_argument, NULL, 'f' },
{ "mapcompat", no_argument, NULL, 'm' },
@@ -485,7 +487,7 @@ int main(int argc, char **argv)
bin_name = "bpftool";
opterr = 0;
- while ((opt = getopt_long(argc, argv, "VhpjfLmndSi:k:B:l",
+ while ((opt = getopt_long(argc, argv, "VhpjrfLmndSi:k:B:l",
options, NULL)) >= 0) {
switch (opt) {
case 'V':
@@ -507,6 +509,9 @@ int main(int argc, char **argv)
}
jsonw_pretty(json_wtr, pretty_output);
break;
+ case 'r':
+ recursive_dump = true;
+ break;
case 'f':
show_pinned = true;
break;
diff --git a/tools/bpf/bpftool/main.h b/tools/bpf/bpftool/main.h
index 78b6e0ebb85..76626642e94 100644
--- a/tools/bpf/bpftool/main.h
+++ b/tools/bpf/bpftool/main.h
@@ -83,6 +83,7 @@ extern const char *bin_name;
extern json_writer_t *json_wtr;
extern bool json_output;
+extern bool recursive_dump;
extern bool show_pinned;
extern bool show_pids;
extern bool block_mount;
diff --git a/tools/bpf/bpftool/map.c b/tools/bpf/bpftool/map.c
index 684a8fb7241..c400b15f16d 100644
--- a/tools/bpf/bpftool/map.c
+++ b/tools/bpf/bpftool/map.c
@@ -17,6 +17,7 @@
#include <bpf/bpf.h>
#include <bpf/btf.h>
#include <bpf/hashmap.h>
+#include <bpf/libbpf_internal.h>
#include "json_writer.h"
#include "main.h"
@@ -826,12 +827,44 @@ static void free_map_kv_btf(struct btf *btf)
btf__free(btf);
}
+struct map_dump_ctx {
+ struct hashmap *seen;
+ __u32 *pending_ids;
+ size_t pending_cnt;
+};
+
+static int collect_inner_map(struct map_dump_ctx *ctx, __u32 id)
+{
+ __u32 *ids;
+ int err;
+
+ if (hashmap__find(ctx->seen, id, NULL))
+ return 0;
+
+ ids = libbpf_reallocarray(ctx->pending_ids, ctx->pending_cnt + 1,
+ sizeof(*ids));
+ if (!ids) {
+ p_err("mem alloc failed");
+ return -1;
+ }
+ ctx->pending_ids = ids;
+
+ err = hashmap__add(ctx->seen, id, 0);
+ if (err) {
+ p_err("failed to record inner map id %u: %s", id, strerror(-err));
+ return -1;
+ }
+ ids[ctx->pending_cnt++] = id;
+ return 0;
+}
+
static int
map_dump(int fd, struct bpf_map_info *info, json_writer_t *wtr,
- bool show_header)
+ bool show_header, struct map_dump_ctx *ctx)
{
void *key, *value, *prev_key;
unsigned int num_elems = 0;
+ json_writer_t *plain_btf_wtr = NULL;
struct btf *btf = NULL;
int err;
@@ -845,6 +878,17 @@ map_dump(int fd, struct bpf_map_info *info, json_writer_t *wtr,
prev_key = NULL;
+ if (ctx && !wtr && (info->btf_value_type_id ||
+ info->btf_vmlinux_value_type_id)) {
+ plain_btf_wtr = get_btf_writer();
+ if (plain_btf_wtr) {
+ if (show_header)
+ show_map_header_plain(info);
+ show_header = false;
+ wtr = plain_btf_wtr;
+ }
+ }
+
if (wtr) {
err = get_map_kv_btf(info, &btf);
if (err) {
@@ -874,10 +918,20 @@ 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;
+ }
+ }
prev_key = key;
}
@@ -894,18 +948,25 @@ map_dump(int fd, struct bpf_map_info *info, json_writer_t *wtr,
free(key);
free(value);
free_map_kv_btf(btf);
+ if (plain_btf_wtr)
+ jsonw_destroy(&plain_btf_wtr);
return err;
}
static int do_dump(int argc, char **argv)
{
+ LIBBPF_OPTS(bpf_get_fd_by_id_opts, opts,
+ .open_flags = BPF_F_RDONLY,
+ );
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;
int err = -1;
+ size_t j;
if (argc != 2)
usage();
@@ -919,9 +980,34 @@ static int do_dump(int argc, char **argv)
if (nb_fds < 1)
goto exit_free;
+ if (recursive_dump) {
+ ctx.seen = hashmap__new(hash_fn_for_key_as_id,
+ equal_fn_for_key_as_id, NULL);
+ if (IS_ERR(ctx.seen)) {
+ ctx.seen = NULL;
+ p_err("failed to create hashmap for recursive dump");
+ goto exit_close;
+ }
+ /* Record the selected maps before discovering any inner maps. */
+ for (i = 0; i < nb_fds; i++) {
+ len = sizeof(info);
+ if (bpf_map_get_info_by_fd(fds[i], &info, &len)) {
+ p_err("can't get map info: %s", strerror(errno));
+ err = -1;
+ goto exit_close;
+ }
+ err = hashmap__add(ctx.seen, info.id, 0);
+ if (err) {
+ p_err("failed to record map id %u: %s", info.id,
+ strerror(-err));
+ goto exit_close;
+ }
+ }
+ }
+
if (json_output) {
wtr = json_wtr;
- } else {
+ } else if (!recursive_dump) {
int do_plain_btf;
do_plain_btf = maps_have_btf(fds, nb_fds);
@@ -936,7 +1022,7 @@ static int do_dump(int argc, char **argv)
}
}
- if (wtr && nb_fds > 1)
+ if (wtr && (nb_fds > 1 || recursive_dump))
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 +1030,50 @@ 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_dump,
+ recursive_dump ? &ctx : NULL);
if (!wtr && i != nb_fds - 1)
printf("\n");
if (err)
break;
- close(fds[i]);
+ /* Keep selected maps alive while visiting their inner maps. */
+ if (!recursive_dump)
+ close(fds[i]);
+ }
+ for (j = 0; !err && j < ctx.pending_cnt; j++) {
+ int fd;
+
+ fd = bpf_map_get_fd_by_id_opts(ctx.pending_ids[j], &opts);
+ if (fd < 0) {
+ p_err("can't open inner map id %u: %s",
+ ctx.pending_ids[j], strerror(errno));
+ err = -1;
+ break;
+ }
+ len = sizeof(info);
+ if (bpf_map_get_info_by_fd(fd, &info, &len)) {
+ p_err("can't get map info: %s", strerror(errno));
+ err = -1;
+ } else {
+ if (!wtr)
+ printf("\n");
+ err = map_dump(fd, &info, wtr, true, &ctx);
+ }
+ close(fd);
}
- if (wtr && nb_fds > 1)
+ if (wtr && (nb_fds > 1 || recursive_dump))
jsonw_end_array(wtr); /* root array */
if (btf_wtr)
jsonw_destroy(&btf_wtr);
exit_close:
+ if (recursive_dump)
+ i = 0;
for (; i < nb_fds; i++)
close(fds[i]);
+ hashmap__free(ctx.seen);
+ free(ctx.pending_ids);
exit_free:
free(fds);
free_btf_vmlinux();
@@ -1484,7 +1598,7 @@ static int do_help(int argc, char **argv)
" task_storage | bloom_filter | user_ringbuf | cgrp_storage | arena |\n"
" insn_array | rhash }\n"
" " HELP_SPEC_OPTIONS " |\n"
- " {-f|--bpffs} | {-n|--nomount} }\n"
+ " {-f|--bpffs} | {-n|--nomount} | {-r|--recursive} }\n"
"",
bin_name, argv[-2]);
--
2.55.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH bpf-next v3 2/2] selftests/bpf: Cover recursive bpftool map dumps
2026-09-07 1:21 ` [PATCH bpf-next v3 0/2] " Tianyi Chen
2026-09-07 1:21 ` [PATCH bpf-next v3 1/2] " Tianyi Chen
@ 2026-09-07 1:21 ` Tianyi Chen
2026-09-07 2:21 ` bot+bpf-ci
1 sibling, 1 reply; 11+ messages in thread
From: Tianyi Chen @ 2026-09-07 1:21 UTC (permalink / raw)
To: bpf; +Cc: Tianyi Chen, Quentin Monnet, linux-kselftest
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.
Cover ordinary and nested perf event arrays to ensure unsupported
element lookups retain their error markers without failing the dump.
Dump 64 distinct inner maps under RLIMIT_NOFILE=32 in both JSON and
plain modes to catch holding every discovered inner-map FD open.
Open 32 extra descriptors before forking, close inherited descriptors
in the child before imposing the limit, and verify the parent's file
descriptors and outer map remain usable.
Assisted-by: Codex:GPT-6
Signed-off-by: Tianyi Chen <hi@tychen.cc>
---
.../bpf/prog_tests/bpftool_map_dump.c | 465 ++++++++++++++++++
1 file changed, 465 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..9c88743a8f8
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/bpftool_map_dump.c
@@ -0,0 +1,465 @@
+// SPDX-License-Identifier: GPL-2.0-only
+#include <test_progs.h>
+#include <bpftool_helpers.h>
+#include <bpf/btf.h>
+#include <sys/resource.h>
+#include <dirent.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]);
+}
+
+static void test_unreadable(bool outer)
+{
+ LIBBPF_OPTS(bpf_map_create_opts, opts);
+ char elements[OUTPUT_SIZE], output[OUTPUT_SIZE], root[OUTPUT_SIZE];
+ char expected[OUTPUT_SIZE * 3], plain[OUTPUT_SIZE] = {};
+ char command[MAX_BPFTOOL_CMD_LEN];
+ int inner_fd = -1, outer_fd = -1, lookup_errno;
+ __u32 inner_id, root_id, key = 0, value;
+
+ /*
+ * Every key is enumerable, but PERF_EVENT_ARRAY lookup returns
+ * ENOTSUPP (the kernel-internal errno). Check both entries so an
+ * early exit on the first lookup failure cannot pass.
+ */
+ inner_fd = bpf_map_create(BPF_MAP_TYPE_PERF_EVENT_ARRAY, "dump_unreadable",
+ sizeof(key), sizeof(value), 2, NULL);
+ if (!ASSERT_OK_FD(inner_fd, "create_unreadable"))
+ goto out;
+ if (!ASSERT_LT(bpf_map_lookup_elem(inner_fd, &key, &value), 0,
+ "unreadable_lookup"))
+ goto out;
+ lookup_errno = errno;
+ if (!ASSERT_NEQ(lookup_errno, ENOENT, "unreadable_not_missing"))
+ goto out;
+ inner_id = map_id(inner_fd);
+ if (!inner_id || !dump_map(inner_id, "-j", elements))
+ goto out;
+ ASSERT_EQ(count_token(elements, "\"error\":"), 2, "default_json_errors");
+ snprintf(command, sizeof(command), "map dump id %u", inner_id);
+ if (!ASSERT_OK(get_bpftool_command_output(command, plain, sizeof(plain) - 1),
+ "default_plain_unreadable"))
+ goto out;
+ ASSERT_EQ(count_token(plain, strerror(lookup_errno)), 2, "default_plain_errors");
+ ASSERT_HAS_SUBSTR(plain, "Found 0 elements", "default_plain_count");
+
+ root_id = inner_id;
+ if (outer) {
+ opts.inner_map_fd = inner_fd;
+ outer_fd = bpf_map_create(BPF_MAP_TYPE_ARRAY_OF_MAPS, "dump_outer",
+ sizeof(key), sizeof(value), 1, &opts);
+ if (!ASSERT_OK_FD(outer_fd, "create_outer") ||
+ !ASSERT_OK(bpf_map_update_elem(outer_fd, &key, &inner_fd, BPF_ANY),
+ "populate_outer"))
+ goto out;
+ root_id = map_id(outer_fd);
+ if (!root_id || !dump_map(root_id, "-j", root))
+ goto out;
+ snprintf(expected, sizeof(expected),
+ "[{\"id\":%u,\"type\":\"array_of_maps\",\"name\":\"dump_outer\","
+ "\"flags\":0,\"elements\":%s},{\"id\":%u,"
+ "\"type\":\"perf_event_array\",\"name\":\"dump_unreadable\","
+ "\"flags\":0,\"elements\":%s}]", root_id, root, inner_id, elements);
+ } else {
+ snprintf(expected, sizeof(expected),
+ "[{\"id\":%u,\"type\":\"perf_event_array\","
+ "\"name\":\"dump_unreadable\",\"flags\":0,\"elements\":%s}]",
+ inner_id, elements);
+ }
+ if (dump_map(root_id, "-j -r", output))
+ ASSERT_STREQ(output, expected, "recursive_unreadable_json");
+ memset(output, 0, sizeof(output));
+ snprintf(command, sizeof(command), "-r map dump id %u", root_id);
+ if (ASSERT_OK(get_bpftool_command_output(command, output, sizeof(output) - 1),
+ "recursive_unreadable_plain")) {
+ ASSERT_HAS_SUBSTR(output, plain, "recursive_plain_preserves_errors");
+ ASSERT_EQ(count_token(output, strerror(lookup_errno)), 2,
+ "recursive_plain_errors");
+ ASSERT_EQ(count_token(output, "Found "), outer ? 2 : 1,
+ "recursive_plain_maps");
+ }
+out:
+ if (outer_fd >= 0)
+ close(outer_fd);
+ if (inner_fd >= 0)
+ close(inner_fd);
+}
+
+static void test_many_inner_maps(bool json)
+{
+ LIBBPF_OPTS(bpf_map_create_opts, opts);
+ const struct rlimit limit = { .rlim_cur = 32, .rlim_max = 32 };
+ char command[MAX_BPFTOOL_CMD_LEN], token[64];
+ __u32 ids[64], root_id, key;
+ int inner_fd = -1, outer_fd = -1, status;
+ int inherited_fds[32], nr_inherited = 0, i;
+ char *output = NULL;
+ pid_t pid;
+
+ inner_fd = bpf_map_create(BPF_MAP_TYPE_HASH, "dump_inner", 4, 4, 1, NULL);
+ if (!ASSERT_OK_FD(inner_fd, "create_template"))
+ goto out;
+ opts.inner_map_fd = inner_fd;
+ outer_fd = bpf_map_create(BPF_MAP_TYPE_ARRAY_OF_MAPS, "dump_outer", 4, 4,
+ ARRAY_SIZE(ids), &opts);
+ close(inner_fd);
+ inner_fd = -1;
+ if (!ASSERT_OK_FD(outer_fd, "create_outer"))
+ goto out;
+ for (key = 0; key < ARRAY_SIZE(ids); key++) {
+ inner_fd = bpf_map_create(BPF_MAP_TYPE_HASH, "dump_inner", 4, 4, 1, NULL);
+ if (!ASSERT_OK_FD(inner_fd, "create_inner") ||
+ !ASSERT_OK(bpf_map_update_elem(outer_fd, &key, &inner_fd, BPF_ANY),
+ "populate_outer"))
+ goto out;
+ ids[key] = map_id(inner_fd);
+ if (!ids[key])
+ goto out;
+ /* The outer map keeps each distinct inner map alive. */
+ close(inner_fd);
+ inner_fd = -1;
+ }
+ root_id = map_id(outer_fd);
+ output = calloc(1, 65536);
+ if (!root_id || !ASSERT_OK_PTR(output, "allocate_output"))
+ goto out;
+
+ /* Fill the low FD slots to exercise inherited descriptor cleanup. */
+ for (i = 0; i < ARRAY_SIZE(inherited_fds); i++) {
+ int fd = open("/dev/null", O_RDONLY);
+
+ if (!ASSERT_OK_FD(fd, "open_inherited_fd"))
+ goto out;
+ inherited_fds[nr_inherited++] = fd;
+ }
+
+ /*
+ * Create all fixtures before lowering the limit, and keep the test
+ * runner's limit unchanged. Retaining every discovered FD would exceed
+ * this limit before the recursive dump could visit all inner maps.
+ */
+ pid = fork();
+ if (!ASSERT_GE(pid, 0, "fork"))
+ goto out;
+ if (!pid) {
+ struct dirent *entry;
+ DIR *dir;
+
+ /* Reserve a slot for the directory even if the parent is full. */
+ close(inherited_fds[nr_inherited - 1]);
+ dir = opendir("/proc/self/fd");
+ if (!dir)
+ _exit(6);
+ /* The parent keeps the outer map and its inner maps alive. */
+ for (;;) {
+ char *end;
+ long fd;
+
+ errno = 0;
+ entry = readdir(dir);
+ if (!entry) {
+ if (errno)
+ _exit(6);
+ break;
+ }
+ fd = strtol(entry->d_name, &end, 10);
+ if (*end || fd < 3 || fd == dirfd(dir))
+ continue;
+ close(fd);
+ }
+ if (closedir(dir))
+ _exit(6);
+ if (setrlimit(RLIMIT_NOFILE, &limit))
+ _exit(1);
+ snprintf(command, sizeof(command), "%s -r map dump id %u",
+ json ? "-j" : "", root_id);
+ if (get_bpftool_command_output(command, output, 65535))
+ _exit(2);
+ if (count_token(output, json ? "\"id\":" : "Found ") != ARRAY_SIZE(ids) + 1 ||
+ count_token(output, json ? "\"inner_map_id\":" : "inner_map_id:") !=
+ ARRAY_SIZE(ids))
+ _exit(3);
+ for (key = 0; key < ARRAY_SIZE(ids); key++) {
+ if (json)
+ snprintf(token, sizeof(token), "\"id\":%u,", ids[key]);
+ else
+ snprintf(token, sizeof(token), "\n%u: hash name dump_inner ",
+ ids[key]);
+ if (count_token(output, token) != 1)
+ _exit(4);
+ }
+ if (json && (output[0] != '[' ||
+ strcmp(output + strlen(output) - 2, "]\n")))
+ _exit(5);
+ _exit(0);
+ }
+ if (ASSERT_EQ(waitpid(pid, &status, 0), pid, "waitpid") &&
+ ASSERT_TRUE(WIFEXITED(status), "child_exited"))
+ ASSERT_EQ(WEXITSTATUS(status), 0, "dump_with_low_fd_limit");
+ for (i = 0; i < nr_inherited; i++)
+ ASSERT_GE(fcntl(inherited_fds[i], F_GETFD), 0, "parent_fd_preserved");
+ ASSERT_EQ(map_id(outer_fd), root_id, "parent_outer_preserved");
+out:
+ while (nr_inherited)
+ close(inherited_fds[--nr_inherited]);
+ free(output);
+ if (outer_fd >= 0)
+ close(outer_fd);
+ if (inner_fd >= 0)
+ close(inner_fd);
+}
+
+void test_bpftool_map_dump(void)
+{
+ if (test__start_subtest("unreadable_ordinary"))
+ test_unreadable(false);
+ if (test__start_subtest("unreadable_inner"))
+ test_unreadable(true);
+ if (test__start_subtest("many_inner_maps_json"))
+ test_many_inner_maps(true);
+ if (test__start_subtest("many_inner_maps_plain"))
+ test_many_inner_maps(false);
+ 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] 11+ messages in thread
* Re: [PATCH bpf-next v3 2/2] selftests/bpf: Cover recursive bpftool map dumps
2026-09-07 1:21 ` [PATCH bpf-next v3 2/2] selftests/bpf: Cover recursive bpftool map dumps Tianyi Chen
@ 2026-09-07 2:21 ` bot+bpf-ci
2026-09-07 5:14 ` Tianyi Chen
0 siblings, 1 reply; 11+ messages in thread
From: bot+bpf-ci @ 2026-09-07 2:21 UTC (permalink / raw)
To: hi, bpf
Cc: hi, qmo, linux-kselftest, ast, andrii, daniel, martin.lau,
eddyz87, yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 1648 bytes --]
> 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 000000000000..9c88743a8f84
> --- /dev/null
> +++ b/tools/testing/selftests/bpf/prog_tests/bpftool_map_dump.c
> @@ -0,0 +1,465 @@
[ ... ]
> +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;
> +}
Is the comment accurate? Looking at the helper's implementation in
tools/testing/selftests/bpf/bpftool_helpers.c, get_bpftool_command_output()
forwards to run_command(), which on the capture path does:
if (!suppress_output) {
n = fread(output_buf, 1, output_max_len - 1, f);
output_buf[n] = '\0';
}
Since dump_map() always passes a non-NULL buffer with a non-zero size,
suppress_output is false, so the helper does NUL-terminate the buffer.
The strcspn() line is only stripping the trailing newline, not adding
termination. Would "The helper leaves the trailing newline in place"
be more accurate?
---
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/34073252721
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH bpf-next v3 1/2] bpftool: Add recursive map dumping
2026-09-07 1:21 ` [PATCH bpf-next v3 1/2] " Tianyi Chen
@ 2026-09-07 2:21 ` bot+bpf-ci
2026-09-07 5:14 ` Tianyi Chen
0 siblings, 1 reply; 11+ messages in thread
From: bot+bpf-ci @ 2026-09-07 2:21 UTC (permalink / raw)
To: hi, bpf
Cc: hi, qmo, linux-kselftest, ast, andrii, daniel, martin.lau,
eddyz87, yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 4626 bytes --]
> commit 29ab48522e4514042981557a125a6b9d94bd4e9c
> Author: Tianyi Chen <hi@tychen.cc>
> Date: Mon Sep 7 09:21:26 2026 +0800
>
> bpftool: Add recursive map dumping
>
> Dumping a map-of-maps currently shows inner map IDs without their
> contents. Add -r/--recursive to dump referenced inner maps as well,
> leaving the default output unchanged.
>
> Show the selected maps followed by discovered inner maps, with each
> map's header and entries. JSON uses an array of these map objects.
> Preserve BTF formatting in plain output, including when typed and
> untyped maps are encountered in the same traversal.
>
> Keep selected map FDs open and queue distinct inner map IDs. Open,
> dump and close each queued map in turn, requiring only one additional
> map FD regardless of the number of inner maps. Preserve per-entry
> lookup error reporting for maps that do not support element lookups.
>
> Report failure if an inner map cannot be opened and close any JSON
> containers before returning. Document deferred ID resolution and the
> non-atomic dump behavior, and add option completion.
This isn't a bug, but could the changelog spend a little less space
restating the hunks and a little more on why the queue-then-reopen
approach was picked over keeping every inner-map FD open? The first
paragraph states the problem and fix well, but the following paragraphs
walk through the diff hunk by hunk, and the final sentence enumerates the
documentation and bash-completion changes themselves. The one piece of
rationale a reviewer would want - why a single reused inner-map FD was
chosen over holding every discovered FD open - is stated as a fact
("requiring only one additional map FD") rather than motivated.
[ ... ]
> diff --git a/tools/bpf/bpftool/map.c b/tools/bpf/bpftool/map.c
> index 684a8fb72414..c400b15f16dd 100644
> --- a/tools/bpf/bpftool/map.c
> +++ b/tools/bpf/bpftool/map.c
[ ... ]
> @@ -919,9 +980,34 @@ static int do_dump(int argc, char **argv)
> if (nb_fds < 1)
> goto exit_free;
>
> + if (recursive_dump) {
> + ctx.seen = hashmap__new(hash_fn_for_key_as_id,
> + equal_fn_for_key_as_id, NULL);
> + if (IS_ERR(ctx.seen)) {
> + ctx.seen = NULL;
> + p_err("failed to create hashmap for recursive dump");
> + goto exit_close;
> + }
> + /* Record the selected maps before discovering any inner maps. */
> + for (i = 0; i < nb_fds; i++) {
> + len = sizeof(info);
> + if (bpf_map_get_info_by_fd(fds[i], &info, &len)) {
> + p_err("can't get map info: %s", strerror(errno));
> + err = -1;
> + goto exit_close;
> + }
> + err = hashmap__add(ctx.seen, info.id, 0);
> + if (err) {
> + p_err("failed to record map id %u: %s", info.id,
> + strerror(-err));
> + goto exit_close;
> + }
> + }
> + }
^^^^
A subsystem pattern flags this as potentially concerning: the pre-pass
assigns the raw negative errno returned by hashmap__add() to err and then
propagates it out of do_dump() as the command return value. Every other
error path in do_dump() and map.c sets err = -1, and the new
collect_inner_map() helper deliberately normalizes the identical
hashmap__add() failure to -1.
A -ENOMEM here would make 'bpftool map dump -r' exit with status 244
instead of the 255 used everywhere else in the file. The -EEXIST case is
unreachable (map_parse_fds() yields maps with distinct ids), so this is
OOM-only and both statuses are non-zero - but it is an inconsistency
introduced by this patch and trivially fixed by 'err = -1;' as in the
sibling helper.
> @@ -936,7 +1022,7 @@ static int do_dump(int argc, char **argv)
> }
> }
>
> - if (wtr && nb_fds > 1)
> + if (wtr && (nb_fds > 1 || recursive_dump))
> jsonw_start_array(wtr); /* root array */
> for (i = 0; i < nb_fds; i++) {
> if (bpf_map_get_info_by_fd(fds[i], &info, &len)) {
This isn't a bug, but in recursive mode the pre-pass and the dump loop
each call bpf_map_get_info_by_fd() on the same fds. Could the pre-pass
hand the ids it already collected to the dump loop instead of querying
every selected map twice? The pre-pass genuinely has to run before any
dumping so that a root map that is also someone's inner map is
de-duplicated, but the info it already fetched could be reused (e.g. by
recording the ids into an array alongside the hashmap) instead of being
re-queried.
---
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/34073252721
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH bpf-next v3 2/2] selftests/bpf: Cover recursive bpftool map dumps
2026-09-07 2:21 ` bot+bpf-ci
@ 2026-09-07 5:14 ` Tianyi Chen
0 siblings, 0 replies; 11+ messages in thread
From: Tianyi Chen @ 2026-09-07 5:14 UTC (permalink / raw)
To: bpf; +Cc: qmo, linux-kselftest, hi
Thanks. The helper on the original base did not NUL-terminate captured
output, but the current bpf-next helper does. The comment should describe
only why this test strips the newline, which is true for both versions.
I have made the following local correction for the next full revision.
It changes no test behavior.
diff --git a/tools/testing/selftests/bpf/prog_tests/bpftool_map_dump.c b/tools/testing/selftests/bpf/prog_tests/bpftool_map_dump.c
index 9c88743a8f8..db547a68d0a 100644
--- a/tools/testing/selftests/bpf/prog_tests/bpftool_map_dump.c
+++ b/tools/testing/selftests/bpf/prog_tests/bpftool_map_dump.c
@@ -16,7 +16,7 @@ static bool dump_map(__u32 id, const char *options, char *output)
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. */
+ /* The helper leaves the trailing newline in place. */
output[strcspn(output, "\n")] = '\0';
return true;
}
^ permalink raw reply related [flat|nested] 11+ messages in thread
* Re: [PATCH bpf-next v3 1/2] bpftool: Add recursive map dumping
2026-09-07 2:21 ` bot+bpf-ci
@ 2026-09-07 5:14 ` Tianyi Chen
0 siblings, 0 replies; 11+ messages in thread
From: Tianyi Chen @ 2026-09-07 5:14 UTC (permalink / raw)
To: bpf; +Cc: qmo, linux-kselftest, hi
Thanks for the review.
I have expanded the local commit message to explain the FD tradeoff:
Holding every discovered inner-map FD open would make descriptor use
grow with the number of maps and could exhaust RLIMIT_NOFILE. Keeping
the selected map FDs open and queuing distinct inner IDs requires only
one additional map FD while traversing them. Deferred ID resolution
means a concurrently removed inner map can disappear before it is
opened; the dump is explicitly not atomic.
For hashmap__add() failure, I am keeping the specific negative errno.
It is already reported with strerror(-err), and the command still
returns failure. I agree that this differs from the nearby -1 paths,
but it does not turn allocation failure into success. I prefer to retain
the specific internal error unless a uniform command exit code is a
maintainer requirement. This is not a claim that a shell preserves the
negative errno: its exit status is limited to eight bits.
For the repeated info lookup, an IDs-only array would not replace the
second query: map_dump() needs the full bpf_map_info, including type,
key/value sizes and BTF information. Caching full records is possible,
but adds allocation and cleanup state. The extra query is once per
selected root, not for every discovered inner map. I am retaining the
simpler traversal state here; this can be optimized separately if that
cost is shown to matter.
^ permalink raw reply [flat|nested] 11+ messages in thread
end of thread, other threads:[~2026-09-07 5:15 UTC | newest]
Thread overview: 11+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-06 15:53 [PATCH bpf-next v2 1/2] bpftool: Add recursive map dumping Tianyi Chen
2026-09-06 15:53 ` [PATCH bpf-next v2 2/2] selftests/bpf: Cover recursive bpftool map dumps Tianyi Chen
2026-09-06 16:18 ` sashiko-bot
2026-09-06 17:02 ` [PATCH bpf-next v2 1/2] bpftool: Add recursive map dumping bot+bpf-ci
2026-09-07 1:21 ` [PATCH bpf-next v3 0/2] " Tianyi Chen
2026-09-07 1:21 ` [PATCH bpf-next v3 1/2] " Tianyi Chen
2026-09-07 2:21 ` bot+bpf-ci
2026-09-07 5:14 ` Tianyi Chen
2026-09-07 1:21 ` [PATCH bpf-next v3 2/2] selftests/bpf: Cover recursive bpftool map dumps Tianyi Chen
2026-09-07 2:21 ` bot+bpf-ci
2026-09-07 5:14 ` Tianyi Chen
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox