bpftool: Add recursive map dumping
Subsystems (4)
v10 cover bpftool: Add recursive map dumping
Rebased on current bpf-next, including Maxim's merged map-dump fixes. The map-info fix and its tests are separate patches 1-2, included here so mixed-BTF dumps also work on kernels without the kernel-side fix.
Changes in v10:
- Split the generic info resets from the recursive implementation.
- Rename the recursive test to avoid the upstream test-file collision.
- Preserve Quentin's Fixes/Acked-by tags and update the ENOTSUPP checks for the merged error-message formatter.
All 21 focused subtests pass on x86-64 KVM with Linux 7.2.13 and 7.3.0. The four map-info regressions fail before the fix and pass after it. All 33 subtests also pass when combined with the batch-dump series.
v9: https://lore.kernel.org/r/[email protected]
Map-info v1: https://lore.kernel.org/r/[email protected]
Tianyi Chen (4):
bpftool: Reset map info before each query in show and dump loops selftests/bpf: Check bpftool map info across mixed BTF maps
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 | 154 ++++- .../prog_tests/bpftool_map_dump_recursive.c | 605 ++++++++++++++++++ .../bpf/prog_tests/bpftool_map_info.c | 127 ++++ 5 files changed, 888 insertions(+), 13 deletions(-) create mode 100644 tools/testing/selftests/bpf/prog_tests/bpftool_map_dump_recursive.c create mode 100644 tools/testing/selftests/bpf/prog_tests/bpftool_map_info.c
base-commit: cd5bc55288d4fd999595c8f2af6a5408cbe92a52
Signature · 1 line
2.56.0
PATCH v10 1/4 bpftool: Reset map info before each query in show and dump loops
On kernels that copy bpf_map_info from userspace without clearing output fields, reusing the structure for a map without BTF retains the preceding map's BTF IDs.
Clear the structure and restore its length before each query in map listing, named-map listing, BTF detection and dumping. This prevents stale BTF IDs and formatting a map with another map's key and value types, including when bpftool runs on kernels without the kernel-side fix.
--- tools/bpf/bpftool/map.c | 8 ++++++++ 1 file changed, 8 insertions(+)
tools/bpf/bpftool/map.c +8 -0
@@ -647,6 +647,8 @@ static int do_show_subset(int argc, char **argv)if (json_output && nb_fds >1 )jsonw_start_array (json_wtr);/* root array */ for (i =0 ; i < nb_fds; i++) { +memset (&info,0 ,sizeof (info)); + len =sizeof (info); err =bpf_map_get_info_by_fd (fds[i], &info, &len);if (err) { p_err("can't get map info: %s",
@@ -718,6 +720,8 @@ static int do_show(int argc, char **argv)break ; } +memset (&info,0 ,sizeof (info)); + len =sizeof (info); err =bpf_map_get_info_by_fd (fd, &info, &len);if (err) {p_err ("can't get map info: %s" ,strerror (errno));
@@ -774,6 +778,8 @@ static int maps_have_btf(int *fds, int nb_fds)int err, i;for (i =0 ; i < nb_fds; i++) { +memset (&info,0 ,sizeof (info)); + len =sizeof (info); err =bpf_map_get_info_by_fd (fds[i], &info, &len);if (err) {p_err ("can't get map info: %s" ,strerror (errno));
@@ -970,6 +976,8 @@ static int do_dump(int argc, char **argv)if (wtr && nb_fds >1 )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 ;
Signature · 1 line
2.56.0
PATCH v10 2/4 selftests/bpf: Check bpftool map info across mixed BTF maps
In reply to Tianyi ChenCreate a typed map followed by a smaller untyped map with the same name. Check that full and named listings do not report BTF for the untyped map, JSON dumps match independently dumped contents, and plain dumps use raw output when the selected maps do not all have BTF.
--- .../bpf/prog_tests/bpftool_map_info.c | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 tools/testing/selftests/bpf/prog_tests/bpftool_map_info.c
tools/testing/selftests/bpf/prog_tests/bpftool_map_info.c new file +127 -0
@@ -0,0 +1,127 @@ +// SPDX-License-Identifier: GPL-2.0-only +#include <test_progs.h> +#include <bpftool_helpers.h> +#include <bpf/btf.h> + +#define OUTPUT_SIZE (1024 * 1024) + +static void check_show (char *output,struct bpf_map_info *info) +{ +char id[32 ]; +char *map, *end, *btf; + +snprintf (id,sizeof (id),"{\"id\":%u," , info->id); + map =strstr (output, id); +if (!ASSERT_OK_PTR (map,"map_in_output" )) +return ; + end =strchr (map,'}' ); +if (!ASSERT_OK_PTR (end,"map_object_end" )) +return ; + btf =strstr (map,"\"btf_id\":" ); +if (info->btf_id) { +ASSERT_TRUE (btf && btf < end,"typed_map_btf" ); +if (btf && btf < end) +ASSERT_EQ (strtoul (btf +strlen ("\"btf_id\":" ),NULL ,10 ), + info->btf_id,"btf_id" ); + }else { +ASSERT_TRUE (!btf || btf > end,"raw_map_no_btf" ); + } +} + +static void test_map_info (const char *operation) +{ +LIBBPF_OPTS (bpf_map_create_opts, opts); +struct bpf_map_info info[2 ] = {}; +char command[MAX_BPFTOOL_CMD_LEN ], name[BPF_OBJ_NAME_LEN ]; +char elements[2 ][1024 ] = {}, expected[4096 ]; +struct btf *btf =NULL ; +int fds[] = {-1 ,-1 }; +__u64 wide_key =0 , wide_value =1 ; +__u32 key =0 , value =2 ; +char *output; +int i; + + output =malloc (OUTPUT_SIZE ); +if (!ASSERT_OK_PTR (output,"output" )) +return ; +snprintf (name,sizeof (name),"info_%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 ; +/* Query the typed map before a smaller map without BTF. */ + fds[0 ] =bpf_map_create (BPF_MAP_TYPE_HASH , name,8 ,8 ,1 , &opts); + fds[1 ] =bpf_map_create (BPF_MAP_TYPE_HASH , name,4 ,4 ,1 ,NULL ); +if (!ASSERT_OK_FD (fds[0 ],"create_typed" ) || + !ASSERT_OK_FD (fds[1 ],"create_raw" ) || + !ASSERT_OK (bpf_map_update_elem (fds[0 ], &wide_key, &wide_value,BPF_ANY ), +"populate_typed" ) || + !ASSERT_OK (bpf_map_update_elem (fds[1 ], &key, &value,BPF_ANY ),"populate_raw" )) + goto out; +for (i =0 ; i <ARRAY_SIZE (fds); i++) { +__u32 len =sizeof (info[i]); + +if (!ASSERT_OK (bpf_map_get_info_by_fd (fds[i], &info[i], &len),"map_info" )) + goto out; + } +if (!ASSERT_LT (info[0 ].id, info[1 ].id,"map_order" )) + goto out; + +if (!strcmp (operation,"show_all" )) +snprintf (command,sizeof (command),"-j map show" ); +else if (!strcmp (operation,"show_name" )) +snprintf (command,sizeof (command),"-j map show name %s" , name); +else +snprintf (command,sizeof (command),"%s map dump name %s" , + !strcmp (operation,"dump_json" ) ?"-j" :"" , name); + output[0 ] ='\0' ; +if (!ASSERT_OK (get_bpftool_command_output (command, output,OUTPUT_SIZE ), operation)) + goto out; +if (!strncmp (operation,"show_" ,5 )) { +check_show (output, &info[0 ]); +check_show (output, &info[1 ]); + }else if (!strcmp (operation,"dump_json" )) { +for (i =0 ; i <ARRAY_SIZE (fds); i++) { +snprintf (command,sizeof (command),"-j map dump id %u" , info[i].id); +if (!ASSERT_OK (get_bpftool_command_output (command, elements[i], +sizeof (elements[i])), +"single_dump" )) + goto out; + elements[i][strcspn (elements[i],"\n" )] ='\0' ; + } +snprintf (expected,sizeof (expected), +"[{\"id\":%u,\"type\":\"hash\",\"name\":\"%s\",\"flags\":0," +"\"elements\":%s},{\"id\":%u,\"type\":\"hash\",\"name\":\"%s\"," +"\"flags\":0,\"elements\":%s}]\n" , + info[0 ].id, name, elements[0 ], info[1 ].id, name, elements[1 ]); +ASSERT_STREQ (output, expected,"independent_map_formatting" ); + }else { +char *count =strstr (output,"Found 1 element\n" ); + +ASSERT_NULL (strstr (output,"\"value\":" ),"mixed_maps_use_raw_output" ); +if (ASSERT_OK_PTR (count,"first_map_count" )) +ASSERT_HAS_SUBSTR (count +1 ,"Found 1 element\n" ,"second_map_count" ); + } +out: +for (i =0 ; i <ARRAY_SIZE (fds); i++) +if (fds[i] >=0 ) +close (fds[i]); +btf__free (btf); +free (output); +} + +void serial_test_bpftool_map_info (void ) +{ +if (test__start_subtest ("show_all" )) +test_map_info ("show_all" ); +if (test__start_subtest ("show_name" )) +test_map_info ("show_name" ); +if (test__start_subtest ("dump_json" )) +test_map_info ("dump_json" ); +if (test__start_subtest ("dump_plain" )) +test_map_info ("dump_plain" ); +}
Signature · 1 line
2.56.0
Re: [PATCH bpf-next v10 2/4] selftests/bpf: Check bpftool map info across mixed BTF maps
Quoted 4 lines · Tianyi Chen
Create a typed map followed by a smaller untyped map with the same name. Check that full and named listings do not report BTF for the untyped map, JSON dumps match independently dumped contents, and plain dumps use raw output when the selected maps do not all have BTF.
This test cannot hit the bug from patch 1. So the four subtests pass with and without patch 1.
pw-bot: cr
PATCH v10 3/4 bpftool: Add recursive map dumping
In reply to Tianyi ChenDumping 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.
--- .../bpf/bpftool/Documentation/bpftool-map.rst | 12 +- tools/bpf/bpftool/bash-completion/bpftool | 3 + tools/bpf/bpftool/map.c | 146 ++++++++++++++++-- 3 files changed, 148 insertions(+), 13 deletions(-)
tools/bpf/bpftool/Documentation/bpftool-map.rst +10 -2
@@ -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*.
tools/bpf/bpftool/bash-completion/bpftool +3 -0
@@ -718,6 +718,9 @@ _bpftool() return 0 ;; *) + if [[ $command == dump && $cword -eq 5 ]]; then + COMPREPLY=( $( compgen -W 'recursive' -- "$cur" ) ) + fi return 0 ;; esac
tools/bpf/bpftool/map.c +135 -11
234 lines not on this page · raw message
Signature · 1 line
2.56.0
Quoted 6 lines · Tianyi Chen
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.
This is overkill.
The kernel has one level of map-in-map. bpf_map_meta_alloc() rejects an inner map that has inner maps. Nothing recurses here.
Lets dump the inner map at the place that prints its id:
open it by id, dump it, close it. That's one more fd too. These go away: struct map_dump_ctx, the hashmap, pending_ids, plain_btf_wtr, the pass that records the selected maps, the second loop in do_dump().
The selected fds don't have to stay open.
this patch inner map in its entry
------------------ ----------------------- ------------------------
json output array of map objects, same entries as without
the reader joins by id 'recursive', plus
"inner_map" from id to open all maps in front of it nothing
in the queue are dumped
inner map is gone the whole dump fails error in this entry,
like a failed lookup
shared inner map printed once printed in every entry
Inner maps get replaced on a live system. That's what map-in-map is for. The longer the queue the more likely the dump fails.
The last row is the price. I think it's fine.
Something like this (not even compile tested):
tools/bpf/bpftool/Documentation/bpftool-map.rst +5 -2
@@ -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,13 @@ 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**, an entry of an **array_of_maps** or a **hash_of_maps** + also has the dump of the inner map that it refers to. + bpftool map update *MAP* [key *DATA*] [value *VALUE*] [*UPDATE_FLAGS*] Update map entry for a given *KEY*.
tools/bpf/bpftool/bash-completion/bpftool +3 -0
@@ -718,6 +718,9 @@ _bpftool() return 0 ;; *) + if [[ $command == dump && $cword -eq 5 ]]; then + COMPREPLY=( $( compgen -W 'recursive' -- "$cur" ) ) + fi return 0 ;; esac
tools/bpf/bpftool/map.c +55 -4
109 lines not on this page · raw message
PATCH v10 4/4 selftests/bpf: Cover recursive bpftool map dumps
In reply to Tianyi ChenExercise 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.
--- .../prog_tests/bpftool_map_dump_recursive.c | 605 ++++++++++++++++++ 1 file changed, 605 insertions(+) create mode 100644 tools/testing/selftests/bpf/prog_tests/bpftool_map_dump_recursive.c
tools/testing/selftests/bpf/prog_tests/bpftool_map_dump_recursive.c new file +605 -0
@@ -0,0 +1,605 @@ +// 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] ="hash" ; + 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; +const char *lookup_error; +__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; + lookup_error =strerror (lookup_errno ==524 ?EOPNOTSUPP : lookup_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, lookup_error),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, lookup_error),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_recursive (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); +}
Signature · 1 line
2.56.0
[email protected]
AI reviewRe: [PATCH bpf-next v10 4/4] selftests/bpf: Cover recursive bpftool map dumps
TL;DR: Test nit only: the expected-output formatting in the new bpftool selftests is duplicated across several tests and has already drifted, so it could move into a small helper.
Quoted 1 line
selftests/bpf: Cover recursive bpftool map dumps
TL;DR: Test nit only: the expected-output formatting in the new bpftool selftests is duplicated across several tests and has already drifted, so it could move into a small helper.
Quoted 5 lines
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 ? "]" : "");This isn't a bug, but could the per-map expected-object formatting in test_multiple_roots() and test_mixed_btf() go into a small helper that appends one map object to the expected buffer?
The two loops are nearly identical and have already drifted: one checks ASSERT_GT(n, 0, "format_expected") with the tag "expected_fits", the other ASSERT_GE(n, 0, "format_expected") with the tag "expected_length", and they differ in whether a trailing newline is appended. The same object format string also appears in test_unreadable() and test_outer().
---
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/41701079493