Linux Kernel Selftest development
 help / color / mirror / Atom feed
* [PATCH bpf-next v9 0/2] bpftool: Add recursive map dumping
@ 2026-09-26  1:42 Tianyi Chen
  2026-09-26  1:42 ` [PATCH bpf-next v9 1/2] " Tianyi Chen
                   ` (2 more replies)
  0 siblings, 3 replies; 6+ messages in thread
From: Tianyi Chen @ 2026-09-26  1:42 UTC (permalink / raw)
  To: bpf; +Cc: qmo, andrii, eddyz87, ihor.solodrai, linux-kselftest,
	skokovmaksimevg

Add an opt-in recursive keyword to dump selected maps followed by their
referenced inner maps.

This version is based on current bpf-next plus Maxim Skokov's v2 patch 1/2,
"bpftool: Fail map dump for maps that can't be iterated":
https://lore.kernel.org/r/20260925162031.27259-2-skokovmaksimevg@gmail.com
That patch is a prerequisite, not part of this series. Keep its early
iteration check before recursive plain/BTF output, and retain its error
reporting. I will handle rebasing this series if the prerequisite changes.

Changes in v9:
- Clear map info and reset its length before each query in all three
  do_dump() loops, preventing BTF metadata from leaking between maps.
- Add mixed BTF/non-BTF root and inner-map tests, with different key/value
  sizes, checking complete JSON and plain output.
- Drop the unnecessary OPTIONS rewrap, as Quentin requested.
- Rebase and resolve the overlap with Maxim's patch as described above.

Validation in an x86-64 VM:
- Both new regression cases fail with v8 and pass with v9.
- All 15 bpftool_map_dump subtests pass on the combined tree.
- Non-iterable ringbuf dumps fail with valid error JSON both with and
  without recursion. The bpftool type-sync and shell syntax checks pass.

v8: https://lore.kernel.org/r/20260925174402.2028649-1-hi@tychen.cc
Review: https://lore.kernel.org/r/43549d30-0f47-46a2-a60f-fb6f252e6738@kernel.org
BTF finding: https://lore.kernel.org/r/06229dde91361ba3f8d3c8d5ab0350f4a66643eb72b0c3554b1c146fe8523eac@mail.kernel.org

Tianyi Chen (2):
  bpftool: Add recursive map dumping
  selftests/bpf: Cover recursive bpftool map dumps

 .../bpf/bpftool/Documentation/bpftool-map.rst |  12 +-
 tools/bpf/bpftool/bash-completion/bpftool     |   3 +
 tools/bpf/bpftool/map.c                       | 148 ++++-
 .../bpf/prog_tests/bpftool_map_dump.c         | 602 ++++++++++++++++++
 4 files changed, 752 insertions(+), 13 deletions(-)
 create mode 100644 tools/testing/selftests/bpf/prog_tests/bpftool_map_dump.c


base-commit: b9f30c763c7ecec41fd3f71b8a5a44971c3fddbf
prerequisite-patch-id: 638622a02f200337c410f2078d38d5cfb9f08404
-- 
2.55.0



^ permalink raw reply	[flat|nested] 6+ messages in thread

* [PATCH bpf-next v9 1/2] bpftool: Add recursive map dumping
  2026-09-26  1:42 [PATCH bpf-next v9 0/2] bpftool: Add recursive map dumping Tianyi Chen
@ 2026-09-26  1:42 ` Tianyi Chen
  2026-09-28 15:25   ` Quentin Monnet
  2026-09-26  1:42 ` [PATCH bpf-next v9 2/2] selftests/bpf: Cover recursive bpftool map dumps Tianyi Chen
       [not found] ` <effbbc956dbdf4c78d27608f6ef8639908323d0aaabf80e1cd592cccac938c99@mail.kernel.org>
  2 siblings, 1 reply; 6+ messages in thread
From: Tianyi Chen @ 2026-09-26  1:42 UTC (permalink / raw)
  To: bpf; +Cc: qmo, andrii, eddyz87, ihor.solodrai, linux-kselftest,
	skokovmaksimevg

Dumping a map-of-maps currently shows inner map IDs without their
contents. Add a recursive keyword to map dump to include referenced
inner maps, leaving the default output unchanged. Show selected maps
followed by distinct inner maps, preserving plain and BTF formatting
and using an array of map objects for JSON output.

Holding every discovered inner-map FD open would make descriptor use
grow with the number of maps and could exhaust RLIMIT_NOFILE. Keep the
selected map FDs open, queue distinct inner map IDs, and open, dump and
close each queued map in turn. This needs only one additional map FD.
The tradeoff is deferred ID resolution: concurrently removed inner
maps can disappear before they are opened, so the dump is not atomic.

Report failure when an inner map cannot be opened, retain per-entry
lookup errors, and close JSON containers before returning.

Link: https://github.com/libbpf/bpftool/issues/58
Assisted-by: LLM
Signed-off-by: Tianyi Chen <hi@tychen.cc>
---
 .../bpf/bpftool/Documentation/bpftool-map.rst |  12 +-
 tools/bpf/bpftool/bash-completion/bpftool     |   3 +
 tools/bpf/bpftool/map.c                       | 148 ++++++++++++++++--
 3 files changed, 150 insertions(+), 13 deletions(-)

diff --git a/tools/bpf/bpftool/Documentation/bpftool-map.rst b/tools/bpf/bpftool/Documentation/bpftool-map.rst
index e4ed7e701c9e..e407d20749df 100644
--- a/tools/bpf/bpftool/Documentation/bpftool-map.rst
+++ b/tools/bpf/bpftool/Documentation/bpftool-map.rst
@@ -29,7 +29,7 @@ MAP COMMANDS
 | **bpftool** **map create**     *FILE* **type** *TYPE* **key** *KEY_SIZE* **value** *VALUE_SIZE* \
 |     **entries** *MAX_ENTRIES* **name** *NAME* [**flags** *FLAGS*] [**inner_map** *MAP*] \
 |     [**offload_dev** *NAME*]
-| **bpftool** **map dump**       *MAP*
+| **bpftool** **map dump**       *MAP* [**recursive**]
 | **bpftool** **map update**     *MAP* [**key** *DATA*] [**value** *VALUE*] [*UPDATE_FLAGS*]
 | **bpftool** **map lookup**     *MAP* [**key** *DATA*]
 | **bpftool** **map getnext**    *MAP* [**key** *DATA*]
@@ -87,10 +87,18 @@ bpftool map create *FILE* type *TYPE* key *KEY_SIZE* value *VALUE_SIZE*  entries
     Keyword **offload_dev** expects a network interface name, and is used to
     request hardware offload for the map.
 
-bpftool map dump    *MAP*
+bpftool map dump    *MAP* [recursive]
     Dump all entries in a given *MAP*.  In case of **name**, *MAP* may match
     several maps which will all be dumped.
 
+    With **recursive**, also dump the inner maps referenced by **array_of_maps**
+    and **hash_of_maps** entries. Each map ID is visited once, even if several
+    entries refer to it. Selected maps are followed by their inner maps.
+
+    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.
+
 bpftool map update  *MAP* [key *DATA*] [value *VALUE*] [*UPDATE_FLAGS*]
     Update map entry for a given *KEY*.
 
diff --git a/tools/bpf/bpftool/bash-completion/bpftool b/tools/bpf/bpftool/bash-completion/bpftool
index 9d9ced270685..1c2febb08254 100644
--- a/tools/bpf/bpftool/bash-completion/bpftool
+++ b/tools/bpf/bpftool/bash-completion/bpftool
@@ -718,6 +718,9 @@ _bpftool()
                             return 0
                             ;;
                         *)
+                            if [[ $command == dump && $cword -eq 5 ]]; then
+                                COMPREPLY=( $( compgen -W 'recursive' -- "$cur" ) )
+                            fi
                             return 0
                             ;;
                     esac
diff --git a/tools/bpf/bpftool/map.c b/tools/bpf/bpftool/map.c
index 626882f2079e..015910a493db 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"
@@ -827,12 +828,43 @@ 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;
+	size_t pending_cap;
+};
+
+static int collect_inner_map(struct map_dump_ctx *ctx, __u32 id)
+{
+	int err;
+
+	if (hashmap__find(ctx->seen, id, NULL))
+		return 0;
+
+	err = libbpf_ensure_mem((void **)&ctx->pending_ids, &ctx->pending_cap,
+				 sizeof(*ctx->pending_ids), ctx->pending_cnt + 1);
+	if (err) {
+		p_err("mem alloc failed");
+		return -1;
+	}
+
+	err = hashmap__add(ctx->seen, id, 0);
+	if (err) {
+		p_err("failed to record inner map id %u: %s", id, strerror(-err));
+		return -1;
+	}
+	ctx->pending_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 *cpu_ids = NULL;
 	int cpu_cnt = 0;
@@ -875,6 +907,17 @@ map_dump(int fd, struct bpf_map_info *info, json_writer_t *wtr,
 		goto exit_free;
 	}
 
+	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) {
@@ -909,9 +952,16 @@ map_dump(int fd, struct bpf_map_info *info, json_writer_t *wtr,
 			p_err("can't get next key: %s", strerror(errno));
 			break;
 		}
-		if (!dump_map_elem(fd, key, value, info, btf, wtr,
-				   cpu_ids, cpu_cnt))
+		err = dump_map_elem(fd, key, value, info, btf, wtr,
+				    cpu_ids, cpu_cnt);
+		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;
 	}
 
@@ -929,21 +979,29 @@ map_dump(int fd, struct bpf_map_info *info, json_writer_t *wtr,
 	free(value);
 	free(cpu_ids);
 	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 = {};
+	bool recursive_dump = false;
 	int nb_fds, i = 0;
 	__u32 len = sizeof(info);
 	int *fds = NULL;
 	int err = -1;
+	size_t j;
 
-	if (argc != 2)
-		usage();
+	if (!REQ_ARGS(2))
+		return -1;
 
 	fds = malloc(sizeof(int));
 	if (!fds) {
@@ -954,9 +1012,46 @@ static int do_dump(int argc, char **argv)
 	if (nb_fds < 1)
 		goto exit_free;
 
+	while (argc > 0) {
+		if (**argv && is_prefix(*argv, "recursive")) {
+			NEXT_ARG();
+			recursive_dump = true;
+		} else {
+			p_err("expected 'recursive', got: '%s'", *argv);
+			goto exit_close;
+		}
+	}
+
+	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++) {
+			memset(&info, 0, sizeof(info));
+			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));
+				err = -1;
+				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);
@@ -971,30 +1066,61 @@ 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++) {
+		memset(&info, 0, sizeof(info));
+		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;
 			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;
+		}
+		memset(&info, 0, sizeof(info));
+		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();
@@ -1506,7 +1632,7 @@ static int do_help(int argc, char **argv)
 		"       %1$s %2$s create     FILE type TYPE key KEY_SIZE value VALUE_SIZE \\\n"
 		"                                  entries MAX_ENTRIES name NAME [flags FLAGS] \\\n"
 		"                                  [inner_map MAP] [offload_dev NAME]\n"
-		"       %1$s %2$s dump       MAP\n"
+		"       %1$s %2$s dump       MAP [recursive]\n"
 		"       %1$s %2$s update     MAP [key DATA] [value VALUE] [UPDATE_FLAGS]\n"
 		"       %1$s %2$s lookup     MAP [key DATA]\n"
 		"       %1$s %2$s getnext    MAP [key DATA]\n"
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [PATCH bpf-next v9 2/2] selftests/bpf: Cover recursive bpftool map dumps
  2026-09-26  1:42 [PATCH bpf-next v9 0/2] bpftool: Add recursive map dumping Tianyi Chen
  2026-09-26  1:42 ` [PATCH bpf-next v9 1/2] " Tianyi Chen
@ 2026-09-26  1:42 ` Tianyi Chen
       [not found] ` <effbbc956dbdf4c78d27608f6ef8639908323d0aaabf80e1cd592cccac938c99@mail.kernel.org>
  2 siblings, 0 replies; 6+ messages in thread
From: Tianyi Chen @ 2026-09-26  1:42 UTC (permalink / raw)
  To: bpf; +Cc: qmo, andrii, eddyz87, ihor.solodrai, linux-kselftest,
	skokovmaksimevg

Exercise array-of-maps and hash-of-maps dumps with shared inner maps,
empty maps, BTF values and multiple selected roots. Check complete JSON
documents, plain output, root ordering, deduplication and unchanged
nonrecursive output. Mix BTF and non-BTF maps with different key and
value sizes among selected roots and discovered inner maps. Check misplaced
keywords and invalid trailing arguments, and accept repeated recursive
keywords.

Cover ordinary and nested perf event arrays to preserve unsupported
lookup error markers. Dump 64 distinct inner maps under RLIMIT_NOFILE=32
to detect retaining a descriptor for every inner map. Check JSON and
plain output, and ensure child descriptor cleanup leaves the parent's
resources usable.

Assisted-by: LLM
Signed-off-by: Tianyi Chen <hi@tychen.cc>
---
 .../bpf/prog_tests/bpftool_map_dump.c         | 602 ++++++++++++++++++
 1 file changed, 602 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 000000000000..4e06a310e303
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/bpftool_map_dump.c
@@ -0,0 +1,602 @@
+// 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
+#define MANY_MAPS_OUTPUT_SIZE 65536
+
+static bool dump_map(__u32 id, const char *options, bool recursive, char *output)
+{
+	char command[MAX_BPFTOOL_CMD_LEN];
+
+	snprintf(command, sizeof(command), "%s map dump id %u%s", options, id,
+		 recursive ? " recursive" : "");
+	memset(output, 0, OUTPUT_SIZE);
+	if (!ASSERT_OK(get_bpftool_command_output(command, output, OUTPUT_SIZE - 1),
+		       "dump_map"))
+		return false;
+	/* The helper leaves the trailing newline in place. */
+	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), "map dump id %u recursive", 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;
+	/* For arrays, unused slots also exercise 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", false, outer) ||
+	    !dump_map(inner_id, "-j", false, 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", true, output))
+		ASSERT_STREQ(output, expected, "recursive_json");
+	if (dump_map(root_id, "--json", true, output))
+		ASSERT_STREQ(output, expected, "recursive_long_json");
+	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", true, 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_mixed_btf(bool inner_maps)
+{
+	LIBBPF_OPTS(bpf_map_create_opts, opts);
+	struct btf *btf = NULL;
+	char command[MAX_BPFTOOL_CMD_LEN], name[BPF_OBJ_NAME_LEN];
+	char output[OUTPUT_SIZE], elements[OUTPUT_SIZE], expected[OUTPUT_SIZE * 4];
+	const char *names[4], *types[4];
+	int fds[] = { -1, -1, -1, -1 };
+	__u64 wide_key = 0, wide_value = 0x01010101;
+	__u32 key = 0, value = 0x01010101, ids[4];
+	int i, n, count = inner_maps ? 4 : 2, first = inner_maps ? 2 : 0;
+	size_t len = 0;
+
+	snprintf(name, sizeof(name), "dump_btf_%u", getpid());
+	btf = btf__new_empty();
+	if (!ASSERT_OK_PTR(btf, "create_btf") ||
+	    !ASSERT_EQ(btf__add_int(btf, "unsigned long long", 8, 0), 1, "btf_int") ||
+	    !ASSERT_OK(btf__load_into_kernel(btf), "load_btf"))
+		goto out;
+	opts.btf_fd = btf__fd(btf);
+	opts.btf_key_type_id = 1;
+	opts.btf_value_type_id = 1;
+	names[first] = inner_maps ? "dump_typed" : name;
+	names[first + 1] = inner_maps ? "dump_raw" : name;
+	types[first] = types[first + 1] = "hash";
+	fds[first] = bpf_map_create(BPF_MAP_TYPE_HASH, names[first], 8, 8, 1, &opts);
+	fds[first + 1] = bpf_map_create(BPF_MAP_TYPE_HASH, names[first + 1], 4, 4, 1, NULL);
+	if (!ASSERT_OK_FD(fds[first], "create_typed") ||
+	    !ASSERT_OK_FD(fds[first + 1], "create_raw") ||
+	    !ASSERT_OK(bpf_map_update_elem(fds[first], &wide_key, &wide_value, BPF_ANY),
+		       "populate_typed") ||
+	    !ASSERT_OK(bpf_map_update_elem(fds[first + 1], &key, &value, BPF_ANY),
+		       "populate_raw"))
+		goto out;
+
+	/* Separate roots allow inner maps with different key and value sizes. */
+	for (i = 0; i < first; i++) {
+		LIBBPF_OPTS(bpf_map_create_opts, outer_opts,
+			   .inner_map_fd = fds[first + i]);
+
+		names[i] = name;
+		types[i] = "array_of_maps";
+		fds[i] = bpf_map_create(BPF_MAP_TYPE_ARRAY_OF_MAPS, name, 4, 4, 1,
+				       &outer_opts);
+		if (!ASSERT_OK_FD(fds[i], "create_outer") ||
+		    !ASSERT_OK(bpf_map_update_elem(fds[i], &key, &fds[first + i], BPF_ANY),
+			       "populate_outer"))
+			goto out;
+	}
+
+	for (i = 0; i < count; i++) {
+		ids[i] = map_id(fds[i]);
+		if (!ids[i] || !dump_map(ids[i], "-j", false, elements))
+			goto out;
+		n = snprintf(expected + len, sizeof(expected) - len,
+			     "%s{\"id\":%u,\"type\":\"%s\",\"name\":\"%s\","
+			     "\"flags\":0,\"elements\":%s}%s",
+			     i ? "," : "[", ids[i], types[i], names[i], elements,
+			     i == count - 1 ? "]\n" : "");
+		if (!ASSERT_GT(n, 0, "format_expected") ||
+		    !ASSERT_LT(n, sizeof(expected) - len, "expected_fits"))
+			goto out;
+		len += n;
+	}
+	snprintf(command, sizeof(command), "-j map dump name %s recursive", name);
+	memset(output, 0, sizeof(output));
+	if (ASSERT_OK(get_bpftool_command_output(command, output, sizeof(output) - 1),
+		      "mixed_json_dump"))
+		ASSERT_STREQ(output, expected, "mixed_json");
+
+	snprintf(command, sizeof(command), "map dump name %s recursive", name);
+	memset(output, 0, sizeof(output));
+	if (ASSERT_OK(get_bpftool_command_output(command, output, sizeof(output) - 1),
+		      "mixed_plain_dump")) {
+		ASSERT_EQ(count_token(output, "Found "), count - 1, "raw_map_count");
+		ASSERT_EQ(count_token(output, "\"value\": 16843009"), 1, "typed_map_count");
+	}
+out:
+	for (i = 0; i < ARRAY_SIZE(fds); i++)
+		if (fds[i] >= 0)
+			close(fds[i]);
+	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;
+	size_t len = 0;
+	int i, n;
+
+	/*
+	 * 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", false, elements))
+			goto out;
+		n = 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 ? "]" : "");
+		if (!ASSERT_GE(n, 0, "format_expected") ||
+		    !ASSERT_LT(n, sizeof(expected) - len, "expected_length"))
+			goto out;
+		len += n;
+	}
+	snprintf(command, sizeof(command), "-j map dump name %s recursive", 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", false, 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", false, 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", true, output))
+		ASSERT_STREQ(output, expected, "recursive_unreadable_json");
+	memset(output, 0, sizeof(output));
+	snprintf(command, sizeof(command), "map dump id %u recursive", 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)
+{
+	enum {
+		DUMP_OK,
+		DUMP_ERR_RLIMIT,
+		DUMP_ERR_COMMAND,
+		DUMP_ERR_COUNTS,
+		DUMP_ERR_IDS,
+		DUMP_ERR_JSON,
+		DUMP_ERR_FDS,
+	};
+	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, MANY_MAPS_OUTPUT_SIZE);
+	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(DUMP_ERR_FDS);
+		/* 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(DUMP_ERR_FDS);
+				break;
+			}
+			fd = strtol(entry->d_name, &end, 10);
+			if (*end || fd < 3 || fd == dirfd(dir))
+				continue;
+			close(fd);
+		}
+		if (closedir(dir))
+			_exit(DUMP_ERR_FDS);
+		if (setrlimit(RLIMIT_NOFILE, &limit))
+			_exit(DUMP_ERR_RLIMIT);
+		snprintf(command, sizeof(command), "%s map dump id %u recursive",
+			 json ? "-j" : "", root_id);
+		if (get_bpftool_command_output(command, output, MANY_MAPS_OUTPUT_SIZE - 1))
+			_exit(DUMP_ERR_COMMAND);
+		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(DUMP_ERR_COUNTS);
+		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(DUMP_ERR_IDS);
+		}
+		if (json && (output[0] != '[' ||
+			     strcmp(output + strlen(output) - 2, "]\n")))
+			_exit(DUMP_ERR_JSON);
+		_exit(DUMP_OK);
+	}
+	if (ASSERT_EQ(waitpid(pid, &status, 0), pid, "waitpid") &&
+	    ASSERT_TRUE(WIFEXITED(status), "child_exited"))
+		ASSERT_EQ(WEXITSTATUS(status), DUMP_OK, "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);
+}
+
+static void test_dump_arguments(void)
+{
+	const char * const options[] = {
+		"''", "unknown", "recursive unknown",
+	};
+	char command[MAX_BPFTOOL_CMD_LEN], output[OUTPUT_SIZE] = {};
+	__u32 id;
+	int fd;
+	size_t i;
+
+	fd = bpf_map_create(BPF_MAP_TYPE_HASH, "dump_args", 4, 4, 1, NULL);
+	if (!ASSERT_OK_FD(fd, "create_map"))
+		return;
+	id = map_id(fd);
+	if (!id)
+		goto out;
+	for (i = 0; i < ARRAY_SIZE(options); i++) {
+		snprintf(command, sizeof(command), "map dump id %u %s", id, options[i]);
+		ASSERT_NEQ(run_bpftool_command(command), 0, options[i]);
+	}
+	snprintf(command, sizeof(command), "map dump id %u recursive recursive", id);
+	ASSERT_OK(run_bpftool_command(command), "repeated_recursive");
+	snprintf(command, sizeof(command), "-j map dump recursive id %u", id);
+	ASSERT_NEQ(get_bpftool_command_output(command, output, sizeof(output)), 0,
+		   "misplaced_recursive");
+	ASSERT_HAS_SUBSTR(output, "expected 'id', 'name' or 'pinned'", "map_first_error");
+	ASSERT_HAS_SUBSTR(output, "got: 'recursive'", "misplaced_keyword");
+out:
+	close(fd);
+}
+
+void test_bpftool_map_dump(void)
+{
+	if (test__start_subtest("mixed_btf_roots"))
+		test_mixed_btf(false);
+	if (test__start_subtest("mixed_btf_inner_maps"))
+		test_mixed_btf(true);
+	if (test__start_subtest("arguments"))
+		test_dump_arguments();
+	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] 6+ messages in thread

* Re: [PATCH bpf-next v9 0/2] bpftool: Add recursive map dumping
       [not found] ` <effbbc956dbdf4c78d27608f6ef8639908323d0aaabf80e1cd592cccac938c99@mail.kernel.org>
@ 2026-09-26  6:08   ` Tianyi Chen
  0 siblings, 0 replies; 6+ messages in thread
From: Tianyi Chen @ 2026-09-26  6:08 UTC (permalink / raw)
  To: qmo, bpf
  Cc: andrii, eddyz87, ihor.solodrai, linux-kselftest, skokovmaksimevg,
	kernel-ci, daniel, martin.lau

Hi Quentin, Maxim,

> Status:     CONFLICT
> Name:       [bpf-next,v9,0/2] bpftool: Add recursive map dumping

v9 depends on Maxim's "bpftool: Fail map dump for maps that can't be
iterated" (v2, patch 1/2):
https://lore.kernel.org/r/20260925162031.27259-2-skokovmaksimevg@gmail.com

The CI application log shows that this prerequisite is absent from its
upstream base, so recursive dump patch 1/2 conflicts. I listed the
dependency in the cover letter, but should also have checked the standalone
application path before sending v9. Sorry for the churn.

The AI review on PR #14100 asks:
> Was a bpftool patch that adds 'recursive' meant to come with this series,
> or to be merged before it?

It is patch 1/2 of this submission. Both patches are present in Patchwork,
but the resulting PR contains only the selftest commit after the failed
application. That review therefore concerns an incomplete tree, not the
full submitted series:
https://github.com/kernel-patches/bpf/pull/14100

Please hold v9 for now. I'll wait for Maxim's prerequisite to land, then
rebase and resend against upstream, checking that the two mailed patches
apply cleanly without any extra prerequisite and rerunning the focused
tests. Maxim, no need to change your rebase plans for my series.

Thanks,
Tianyi

^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [PATCH bpf-next v9 1/2] bpftool: Add recursive map dumping
  2026-09-26  1:42 ` [PATCH bpf-next v9 1/2] " Tianyi Chen
@ 2026-09-28 15:25   ` Quentin Monnet
  2026-09-28 17:19     ` Tianyi Chen
  0 siblings, 1 reply; 6+ messages in thread
From: Quentin Monnet @ 2026-09-28 15:25 UTC (permalink / raw)
  To: Tianyi Chen, bpf
  Cc: andrii, eddyz87, ihor.solodrai, linux-kselftest, skokovmaksimevg

2026-09-26 10:42 UTC+0900 ~ Tianyi Chen <hi@tychen.cc>
> Dumping a map-of-maps currently shows inner map IDs without their
> contents. Add a recursive keyword to map dump to include referenced
> inner maps, leaving the default output unchanged. Show selected maps
> followed by distinct inner maps, preserving plain and BTF formatting
> and using an array of map objects for JSON output.
> 
> Holding every discovered inner-map FD open would make descriptor use
> grow with the number of maps and could exhaust RLIMIT_NOFILE. Keep the
> selected map FDs open, queue distinct inner map IDs, and open, dump and
> close each queued map in turn. This needs only one additional map FD.
> The tradeoff is deferred ID resolution: concurrently removed inner
> maps can disappear before they are opened, so the dump is not atomic.
> 
> Report failure when an inner map cannot be opened, retain per-entry
> lookup errors, and close JSON containers before returning.
> 
> Link: https://github.com/libbpf/bpftool/issues/58
> Assisted-by: LLM
> Signed-off-by: Tianyi Chen <hi@tychen.cc>

> diff --git a/tools/bpf/bpftool/map.c b/tools/bpf/bpftool/map.c
> index 626882f2079e..015910a493db 100644
> --- a/tools/bpf/bpftool/map.c
> +++ b/tools/bpf/bpftool/map.c

> @@ -971,30 +1066,61 @@ 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++) {
> +		memset(&info, 0, sizeof(info));
> +		len = sizeof(info);


Good catch, reading bpf-ci's review the other day I didn't realise there
were other locations in the file where we omitted to reset "info", which
means we may print wrong BTF info when dumping the maps, that's a bug.

I can see three other locations (do_show(), do_show_subset(),
maps_have_btf()) where we call bpf_map_get_info_by_fd() in loops without
resetting "info". We'll need to fix these, but this should be a
different patch or series. You're welcome to go at it if you want, I'll
send a patch otherwise, just let me know. I would maybe move this
memset() (the hunk above) to that dedicated patch.

This patch looks good to me otherwise:

Acked-by: Quentin Monnet <qmo@kernel.org>

You still have a conflict with Maxim's series on patch 2 though, both of
you picked the same test file name. Maybe use
bpftool_map_dump_recursive.c instead?

Thanks,
Quentin

^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [PATCH bpf-next v9 1/2] bpftool: Add recursive map dumping
  2026-09-28 15:25   ` Quentin Monnet
@ 2026-09-28 17:19     ` Tianyi Chen
  0 siblings, 0 replies; 6+ messages in thread
From: Tianyi Chen @ 2026-09-28 17:19 UTC (permalink / raw)
  To: Quentin Monnet, bpf
  Cc: andrii, eddyz87, ihor.solodrai, linux-kselftest, skokovmaksimevg

Hi Quentin,

> We'll need to fix these, but this should be a
> different patch or series. You're welcome to go at it if you want, I'll
> send a patch otherwise, just let me know.

I've taken care of this in a separate two-patch series:
https://lore.kernel.org/r/cover.1790615923.git.hi@tychen.cc

It clears info and resets len before each query in do_show(),
do_show_subset(), maps_have_btf(), and the existing do_dump() loop.
The four regression subtests fail before the fix and pass after it.

> I would maybe move this
> memset() (the hunk above) to that dedicated patch.

Done. The resets in the two new recursive-only loops stay with the
recursive implementation.

> You still have a conflict with Maxim's series on patch 2 though, both of
> you picked the same test file name. Maybe use
> bpftool_map_dump_recursive.c instead?

Done locally, including renaming the test entry point. I tested against
Maxim's complete v4 series, keeping his tests intact: all 21 focused
subtests pass in an x86-64 VM.

Thanks for the Ack. I'll carry it forward and wait for the prerequisites
to land before rebasing and resending the recursive series.

Thanks,
Tianyi

^ permalink raw reply	[flat|nested] 6+ messages in thread

end of thread, other threads:[~2026-09-28 17:20 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-26  1:42 [PATCH bpf-next v9 0/2] bpftool: Add recursive map dumping Tianyi Chen
2026-09-26  1:42 ` [PATCH bpf-next v9 1/2] " Tianyi Chen
2026-09-28 15:25   ` Quentin Monnet
2026-09-28 17:19     ` Tianyi Chen
2026-09-26  1:42 ` [PATCH bpf-next v9 2/2] selftests/bpf: Cover recursive bpftool map dumps Tianyi Chen
     [not found] ` <effbbc956dbdf4c78d27608f6ef8639908323d0aaabf80e1cd592cccac938c99@mail.kernel.org>
2026-09-26  6:08   ` [PATCH bpf-next v9 0/2] bpftool: Add recursive map dumping Tianyi Chen

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox