[PATCH bpf-next 1/2] bpftool: Use batch lookups for bounded hash map dumps

From: Tianyi Chen

Date: Sun Sep 06 2026 - 13:11:47 EST


Use BPF_MAP_LOOKUP_BATCH when dumping hash maps to reduce the number
of BPF syscalls. Share element formatting with individual lookups to
preserve plain, JSON and BTF output.

Start with up to 256 entries and grow on ENOSPC without advancing the
input cursor. Restrict the optimization to maps whose maximum key and
value storage fits in 4 MiB, so even a worst-case bucket can fit without
restarting a partially printed dump. Preserve aligned element buffers
for formatting keys and values with odd sizes.

Fall back to individual lookups only if the initial batch operation is
unsupported. Process the final partial batch on ENOENT, but never use
count or output buffers after other errors. Report errors after batch
traversal starts without restarting and duplicating output.

Assisted-by: Codex:GPT-6
Signed-off-by: Tianyi Chen <hi@xxxxxxxxx>
---
tools/bpf/bpftool/map.c | 113 +++++++++++++++++++++++++++++++++++++---
1 file changed, 105 insertions(+), 8 deletions(-)

diff --git a/tools/bpf/bpftool/map.c b/tools/bpf/bpftool/map.c
index 684a8fb7241..1eccdc0d1e9 100644
--- a/tools/bpf/bpftool/map.c
+++ b/tools/bpf/bpftool/map.c
@@ -740,15 +740,10 @@ static int do_show(int argc, char **argv)
return errno == ENOENT ? 0 : -1;
}

-static int dump_map_elem(int fd, void *key, void *value,
- struct bpf_map_info *map_info, struct btf *btf,
- json_writer_t *btf_wtr)
+static void print_map_elem(void *key, void *value,
+ struct bpf_map_info *map_info, struct btf *btf,
+ json_writer_t *btf_wtr)
{
- if (bpf_map_lookup_elem(fd, key, value)) {
- print_entry_error(map_info, key, errno);
- return -1;
- }
-
if (json_output) {
print_entry_json(map_info, key, value, btf);
} else if (btf) {
@@ -762,10 +757,108 @@ static int dump_map_elem(int fd, void *key, void *value,
} else {
print_entry_plain(map_info, key, value);
}
+}
+
+static int dump_map_elem(int fd, void *key, void *value,
+ struct bpf_map_info *map_info, struct btf *btf,
+ json_writer_t *btf_wtr)
+{
+ if (bpf_map_lookup_elem(fd, key, value)) {
+ print_entry_error(map_info, key, errno);
+ return -1;
+ }

+ print_map_elem(key, value, map_info, btf, btf_wtr);
return 0;
}

+#define MAP_DUMP_BATCH_SIZE 256U
+#define MAP_DUMP_BATCH_MAX_BYTES (4 * 1024 * 1024)
+
+/* Return 1 to use individual lookups, but only before batch traversal starts. */
+static int dump_map_batch(int fd, void *key, void *value,
+ struct bpf_map_info *info, struct btf *btf,
+ json_writer_t *wtr, unsigned int *num_elems)
+{
+ __u32 capacity, count, batch = 0, next_batch = 0, i;
+ void *keys = NULL, *values = NULL, *buf;
+ bool first = true, can_fallback = true;
+ int err;
+
+ /* Hash lookup batches must accommodate a whole bucket. Restrict the
+ * optimization to maps whose worst-case bucket fits the memory budget,
+ * so a later ENOSPC never forces a restart after printing some entries.
+ * Division also bounds the allocation multiplications on 32-bit hosts.
+ */
+ if (info->type != BPF_MAP_TYPE_HASH || !info->max_entries ||
+ (__u64)info->key_size + info->value_size >
+ MAP_DUMP_BATCH_MAX_BYTES / info->max_entries)
+ return 1;
+
+ capacity = min(info->max_entries, MAP_DUMP_BATCH_SIZE);
+resize:
+ buf = realloc(keys, (size_t)capacity * info->key_size);
+ if (!buf) {
+ err = ENOMEM;
+ goto error;
+ }
+ keys = buf;
+ buf = realloc(values, (size_t)capacity * info->value_size);
+ if (!buf) {
+ err = ENOMEM;
+ goto error;
+ }
+ values = buf;
+
+ while (true) {
+ count = capacity;
+ err = bpf_map_lookup_batch(fd, first ? NULL : &batch,
+ &next_batch, keys, values, &count, NULL);
+ err = err ? errno : 0;
+ /* Older kernels reject the command before updating count. Do not
+ * inspect the buffers on these errors, or fall back after progress.
+ */
+ if (can_fallback && (err == EINVAL || err == EOPNOTSUPP ||
+ err == 524 /* ENOTSUPP */)) {
+ err = 1;
+ goto out;
+ }
+ can_fallback = false;
+ if (err == ENOSPC) {
+ if (capacity == info->max_entries)
+ goto error;
+ capacity += min(capacity, info->max_entries - capacity);
+ /* Preserve the input cursor: the oversized bucket was not read. */
+ goto resize;
+ }
+ /* In particular, EFAULT can leave count and the buffers invalid. */
+ if (err && err != ENOENT)
+ goto error;
+ for (i = 0; i < count; i++) {
+ /* Keep the alignment provided by individual lookups, including
+ * for BTF types whose map key/value size is not aligned.
+ */
+ memcpy(key, keys + (size_t)i * info->key_size, info->key_size);
+ memcpy(value, values + (size_t)i * info->value_size, info->value_size);
+ print_map_elem(key, value, info, btf, wtr);
+ (*num_elems)++;
+ }
+ if (err == ENOENT) {
+ err = 0;
+ goto out;
+ }
+ first = false;
+ batch = next_batch;
+ }
+error:
+ p_err("can't lookup map batch: %s", strerror(err));
+ err = -1;
+out:
+ free(keys);
+ free(values);
+ return err;
+}
+
static int maps_have_btf(int *fds, int nb_fds)
{
struct bpf_map_info info = {};
@@ -869,6 +962,9 @@ map_dump(int fd, struct bpf_map_info *info, json_writer_t *wtr,
p_info("Warning: cannot read values from %s map with value_size != 8",
map_type_str);
}
+ err = dump_map_batch(fd, key, value, info, btf, wtr, &num_elems);
+ if (err != 1)
+ goto end_dump;
while (true) {
err = bpf_map_get_next_key(fd, prev_key, key);
if (err) {
@@ -881,6 +977,7 @@ map_dump(int fd, struct bpf_map_info *info, json_writer_t *wtr,
prev_key = key;
}

+end_dump:
if (wtr) {
jsonw_end_array(wtr); /* elements */
if (show_header)
--
2.55.0