Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux

selftests/bpf: Add test for bpftool access to read-only protected maps

Add selftest cases that validate bpftool's expected behavior when
accessing maps protected from modification via security_bpf_map.

The test includes a BPF program attached to security_bpf_map with two maps:
- A protected map that only allows read-only access
- An unprotected map that allows full access

The test script attaches the BPF program to security_bpf_map and
verifies that for the bpftool map command:
- Read access works on both maps
- Write access fails on the protected map
- Write access succeeds on the unprotected map
- These behaviors remain consistent when the maps are pinned

Signed-off-by: Slava Imameev <slava.imameev@crowdstrike.com>
Reviewed-by: Quentin Monnet <qmo@kernel.org>
Link: https://lore.kernel.org/r/20250620151812.13952-2-slava.imameev@crowdstrike.com
Signed-off-by: Alexei Starovoitov <ast@kernel.org>

authored by

Slava Imameev and committed by
Alexei Starovoitov
f8b19aec d32179e8

+490
+1
tools/testing/selftests/bpf/Makefile
··· 109 109 test_xdping.sh \ 110 110 test_bpftool_build.sh \ 111 111 test_bpftool.sh \ 112 + test_bpftool_map.sh \ 112 113 test_bpftool_metadata.sh \ 113 114 test_doc_build.sh \ 114 115 test_xsk.sh \
+22
tools/testing/selftests/bpf/progs/bpf_iter_map_elem.c
··· 1 + // SPDX-License-Identifier: GPL-2.0-only 2 + 3 + #include "vmlinux.h" 4 + #include <bpf/bpf_tracing.h> 5 + #include <bpf/bpf_helpers.h> 6 + 7 + char _license[] SEC("license") = "GPL"; 8 + 9 + __u32 value_sum = 0; 10 + 11 + SEC("iter/bpf_map_elem") 12 + int dump_bpf_map_values(struct bpf_iter__bpf_map_elem *ctx) 13 + { 14 + __u32 value = 0; 15 + 16 + if (ctx->value == (void *)0) 17 + return 0; 18 + 19 + bpf_probe_read_kernel(&value, sizeof(value), ctx->value); 20 + value_sum += value; 21 + return 0; 22 + }
+69
tools/testing/selftests/bpf/progs/security_bpf_map.c
··· 1 + // SPDX-License-Identifier: GPL-2.0-only 2 + 3 + #include "vmlinux.h" 4 + #include <bpf/bpf_tracing.h> 5 + #include <bpf/bpf_helpers.h> 6 + 7 + char _license[] SEC("license") = "GPL"; 8 + 9 + #define EPERM 1 /* Operation not permitted */ 10 + 11 + /* From include/linux/mm.h. */ 12 + #define FMODE_WRITE 0x2 13 + 14 + struct map; 15 + 16 + struct { 17 + __uint(type, BPF_MAP_TYPE_ARRAY); 18 + __type(key, __u32); 19 + __type(value, __u32); 20 + __uint(max_entries, 1); 21 + } prot_status_map SEC(".maps"); 22 + 23 + struct { 24 + __uint(type, BPF_MAP_TYPE_HASH); 25 + __type(key, __u32); 26 + __type(value, __u32); 27 + __uint(max_entries, 3); 28 + } prot_map SEC(".maps"); 29 + 30 + struct { 31 + __uint(type, BPF_MAP_TYPE_HASH); 32 + __type(key, __u32); 33 + __type(value, __u32); 34 + __uint(max_entries, 3); 35 + } not_prot_map SEC(".maps"); 36 + 37 + SEC("fmod_ret/security_bpf_map") 38 + int BPF_PROG(fmod_bpf_map, struct bpf_map *map, int fmode) 39 + { 40 + __u32 key = 0; 41 + __u32 *status_ptr = bpf_map_lookup_elem(&prot_status_map, &key); 42 + 43 + if (!status_ptr || !*status_ptr) 44 + return 0; 45 + 46 + if (map == &prot_map) { 47 + /* Allow read-only access */ 48 + if (fmode & FMODE_WRITE) 49 + return -EPERM; 50 + } 51 + 52 + return 0; 53 + } 54 + 55 + /* 56 + * This program keeps references to maps. This is needed to prevent 57 + * optimizing them out. 58 + */ 59 + SEC("fentry/bpf_fentry_test1") 60 + int BPF_PROG(fentry_dummy1, int a) 61 + { 62 + __u32 key = 0; 63 + __u32 val1 = a; 64 + __u32 val2 = a + 1; 65 + 66 + bpf_map_update_elem(&prot_map, &key, &val1, BPF_ANY); 67 + bpf_map_update_elem(&not_prot_map, &key, &val2, BPF_ANY); 68 + return 0; 69 + }
+398
tools/testing/selftests/bpf/test_bpftool_map.sh
··· 1 + #!/bin/sh 2 + # SPDX-License-Identifier: GPL-2.0 3 + 4 + # Kselftest framework requirement - SKIP code is 4. 5 + ksft_skip=4 6 + 7 + TESTNAME="bpftool_map" 8 + BPF_FILE="security_bpf_map.bpf.o" 9 + BPF_ITER_FILE="bpf_iter_map_elem.bpf.o" 10 + PROTECTED_MAP_NAME="prot_map" 11 + NOT_PROTECTED_MAP_NAME="not_prot_map" 12 + BPF_FS_TMP_PARENT="/tmp" 13 + BPF_FS_PARENT=$(awk '$3 == "bpf" {print $2; exit}' /proc/mounts) 14 + BPF_FS_PARENT=${BPF_FS_PARENT:-$BPF_FS_TMP_PARENT} 15 + # bpftool will mount bpf file system under BPF_DIR if it is not mounted 16 + # under BPF_FS_PARENT. 17 + BPF_DIR="$BPF_FS_PARENT/test_$TESTNAME" 18 + SCRIPT_DIR=$(dirname $(realpath "$0")) 19 + BPF_FILE_PATH="$SCRIPT_DIR/$BPF_FILE" 20 + BPF_ITER_FILE_PATH="$SCRIPT_DIR/$BPF_ITER_FILE" 21 + BPFTOOL_PATH="bpftool" 22 + # Assume the script is located under tools/testing/selftests/bpf/ 23 + KDIR_ROOT_DIR=$(realpath "$SCRIPT_DIR"/../../../../) 24 + 25 + _cleanup() 26 + { 27 + set +eu 28 + 29 + # If BPF_DIR is a mount point this will not remove the mount point itself. 30 + [ -d "$BPF_DIR" ] && rm -rf "$BPF_DIR" 2> /dev/null 31 + 32 + # Unmount if BPF filesystem was temporarily created. 33 + if [ "$BPF_FS_PARENT" = "$BPF_FS_TMP_PARENT" ]; then 34 + # A loop and recursive unmount are required as bpftool might 35 + # create multiple mounts. For example, a bind mount of the directory 36 + # to itself. The bind mount is created to change mount propagation 37 + # flags on an actual mount point. 38 + max_attempts=3 39 + attempt=0 40 + while mountpoint -q "$BPF_DIR" && [ $attempt -lt $max_attempts ]; do 41 + umount -R "$BPF_DIR" 2>/dev/null 42 + attempt=$((attempt+1)) 43 + done 44 + 45 + # The directory still exists. Remove it now. 46 + [ -d "$BPF_DIR" ] && rm -rf "$BPF_DIR" 2>/dev/null 47 + fi 48 + } 49 + 50 + cleanup_skip() 51 + { 52 + echo "selftests: $TESTNAME [SKIP]" 53 + _cleanup 54 + 55 + exit $ksft_skip 56 + } 57 + 58 + cleanup() 59 + { 60 + if [ "$?" = 0 ]; then 61 + echo "selftests: $TESTNAME [PASS]" 62 + else 63 + echo "selftests: $TESTNAME [FAILED]" 64 + fi 65 + _cleanup 66 + } 67 + 68 + check_root_privileges() { 69 + if [ $(id -u) -ne 0 ]; then 70 + echo "Need root privileges" 71 + exit $ksft_skip 72 + fi 73 + } 74 + 75 + # Function to verify bpftool path. 76 + # Parameters: 77 + # $1: bpftool path 78 + verify_bpftool_path() { 79 + local bpftool_path="$1" 80 + if ! "$bpftool_path" version > /dev/null 2>&1; then 81 + echo "Could not run test without bpftool" 82 + exit $ksft_skip 83 + fi 84 + } 85 + 86 + # Function to verify BTF support. 87 + # The test requires BTF support for fmod_ret programs. 88 + verify_btf_support() { 89 + if [ ! -f /sys/kernel/btf/vmlinux ]; then 90 + echo "Could not run test without BTF support" 91 + exit $ksft_skip 92 + fi 93 + } 94 + 95 + # Function to initialize map entries with keys [0..2] and values set to 0. 96 + # Parameters: 97 + # $1: Map name 98 + # $2: bpftool path 99 + initialize_map_entries() { 100 + local map_name="$1" 101 + local bpftool_path="$2" 102 + 103 + for key in 0 1 2; do 104 + "$bpftool_path" map update name "$map_name" key $key 0 0 0 value 0 0 0 $key 105 + done 106 + } 107 + 108 + # Test read access to the map. 109 + # Parameters: 110 + # $1: Name command (name/pinned) 111 + # $2: Map name 112 + # $3: bpftool path 113 + # $4: key 114 + access_for_read() { 115 + local name_cmd="$1" 116 + local map_name="$2" 117 + local bpftool_path="$3" 118 + local key="$4" 119 + 120 + # Test read access to the map. 121 + if ! "$bpftool_path" map lookup "$name_cmd" "$map_name" key $key 1>/dev/null; then 122 + echo " Read access to $key in $map_name failed" 123 + exit 1 124 + fi 125 + 126 + # Test read access to map's BTF data. 127 + if ! "$bpftool_path" btf dump map "$name_cmd" "$map_name" 1>/dev/null; then 128 + echo " Read access to $map_name for BTF data failed" 129 + exit 1 130 + fi 131 + } 132 + 133 + # Test write access to the map. 134 + # Parameters: 135 + # $1: Name command (name/pinned) 136 + # $2: Map name 137 + # $3: bpftool path 138 + # $4: key 139 + # $5: Whether write should succeed (true/false) 140 + access_for_write() { 141 + local name_cmd="$1" 142 + local map_name="$2" 143 + local bpftool_path="$3" 144 + local key="$4" 145 + local write_should_succeed="$5" 146 + local value="1 1 1 1" 147 + 148 + if "$bpftool_path" map update "$name_cmd" "$map_name" key $key value \ 149 + $value 2>/dev/null; then 150 + if [ "$write_should_succeed" = "false" ]; then 151 + echo " Write access to $key in $map_name succeeded but should have failed" 152 + exit 1 153 + fi 154 + else 155 + if [ "$write_should_succeed" = "true" ]; then 156 + echo " Write access to $key in $map_name failed but should have succeeded" 157 + exit 1 158 + fi 159 + fi 160 + } 161 + 162 + # Test entry deletion for the map. 163 + # Parameters: 164 + # $1: Name command (name/pinned) 165 + # $2: Map name 166 + # $3: bpftool path 167 + # $4: key 168 + # $5: Whether write should succeed (true/false) 169 + access_for_deletion() { 170 + local name_cmd="$1" 171 + local map_name="$2" 172 + local bpftool_path="$3" 173 + local key="$4" 174 + local write_should_succeed="$5" 175 + local value="1 1 1 1" 176 + 177 + # Test deletion by key for the map. 178 + # Before deleting, check the key exists. 179 + if ! "$bpftool_path" map lookup "$name_cmd" "$map_name" key $key 1>/dev/null; then 180 + echo " Key $key does not exist in $map_name" 181 + exit 1 182 + fi 183 + 184 + # Delete by key. 185 + if "$bpftool_path" map delete "$name_cmd" "$map_name" key $key 2>/dev/null; then 186 + if [ "$write_should_succeed" = "false" ]; then 187 + echo " Deletion for $key in $map_name succeeded but should have failed" 188 + exit 1 189 + fi 190 + else 191 + if [ "$write_should_succeed" = "true" ]; then 192 + echo " Deletion for $key in $map_name failed but should have succeeded" 193 + exit 1 194 + fi 195 + fi 196 + 197 + # After deleting, check the entry existence according to the expected status. 198 + if "$bpftool_path" map lookup "$name_cmd" "$map_name" key $key 1>/dev/null; then 199 + if [ "$write_should_succeed" = "true" ]; then 200 + echo " Key $key for $map_name was not deleted but should have been deleted" 201 + exit 1 202 + fi 203 + else 204 + if [ "$write_should_succeed" = "false" ]; then 205 + echo "Key $key for $map_name was deleted but should have not been deleted" 206 + exit 1 207 + fi 208 + fi 209 + 210 + # Test creation of map's deleted entry, if deletion was successful. 211 + # Otherwise, the entry exists. 212 + if "$bpftool_path" map update "$name_cmd" "$map_name" key $key value \ 213 + $value 2>/dev/null; then 214 + if [ "$write_should_succeed" = "false" ]; then 215 + echo " Write access to $key in $map_name succeeded after deletion attempt but should have failed" 216 + exit 1 217 + fi 218 + else 219 + if [ "$write_should_succeed" = "true" ]; then 220 + echo " Write access to $key in $map_name failed after deletion attempt but should have succeeded" 221 + exit 1 222 + fi 223 + fi 224 + } 225 + 226 + # Test map elements iterator. 227 + # Parameters: 228 + # $1: Name command (name/pinned) 229 + # $2: Map name 230 + # $3: bpftool path 231 + # $4: BPF_DIR 232 + # $5: bpf iterator object file path 233 + iterate_map_elem() { 234 + local name_cmd="$1" 235 + local map_name="$2" 236 + local bpftool_path="$3" 237 + local bpf_dir="$4" 238 + local bpf_file="$5" 239 + local pin_path="$bpf_dir/map_iterator" 240 + 241 + "$bpftool_path" iter pin "$bpf_file" "$pin_path" map "$name_cmd" "$map_name" 242 + if [ ! -f "$pin_path" ]; then 243 + echo " Failed to pin iterator to $pin_path" 244 + exit 1 245 + fi 246 + 247 + cat "$pin_path" 1>/dev/null 248 + rm "$pin_path" 2>/dev/null 249 + } 250 + 251 + # Function to test map access with configurable write expectations 252 + # Parameters: 253 + # $1: Name command (name/pinned) 254 + # $2: Map name 255 + # $3: bpftool path 256 + # $4: key for rw 257 + # $5: key to delete 258 + # $6: Whether write should succeed (true/false) 259 + # $7: BPF_DIR 260 + # $8: bpf iterator object file path 261 + access_map() { 262 + local name_cmd="$1" 263 + local map_name="$2" 264 + local bpftool_path="$3" 265 + local key_for_rw="$4" 266 + local key_to_del="$5" 267 + local write_should_succeed="$6" 268 + local bpf_dir="$7" 269 + local bpf_iter_file_path="$8" 270 + 271 + access_for_read "$name_cmd" "$map_name" "$bpftool_path" "$key_for_rw" 272 + access_for_write "$name_cmd" "$map_name" "$bpftool_path" "$key_for_rw" \ 273 + "$write_should_succeed" 274 + access_for_deletion "$name_cmd" "$map_name" "$bpftool_path" "$key_to_del" \ 275 + "$write_should_succeed" 276 + iterate_map_elem "$name_cmd" "$map_name" "$bpftool_path" "$bpf_dir" \ 277 + "$bpf_iter_file_path" 278 + } 279 + 280 + # Function to test map access with configurable write expectations 281 + # Parameters: 282 + # $1: Map name 283 + # $2: bpftool path 284 + # $3: BPF_DIR 285 + # $4: Whether write should succeed (true/false) 286 + # $5: bpf iterator object file path 287 + test_map_access() { 288 + local map_name="$1" 289 + local bpftool_path="$2" 290 + local bpf_dir="$3" 291 + local pin_path="$bpf_dir/${map_name}_pinned" 292 + local write_should_succeed="$4" 293 + local bpf_iter_file_path="$5" 294 + 295 + # Test access to the map by name. 296 + access_map "name" "$map_name" "$bpftool_path" "0 0 0 0" "1 0 0 0" \ 297 + "$write_should_succeed" "$bpf_dir" "$bpf_iter_file_path" 298 + 299 + # Pin the map to the BPF filesystem 300 + "$bpftool_path" map pin name "$map_name" "$pin_path" 301 + if [ ! -e "$pin_path" ]; then 302 + echo " Failed to pin $map_name" 303 + exit 1 304 + fi 305 + 306 + # Test access to the pinned map. 307 + access_map "pinned" "$pin_path" "$bpftool_path" "0 0 0 0" "2 0 0 0" \ 308 + "$write_should_succeed" "$bpf_dir" "$bpf_iter_file_path" 309 + } 310 + 311 + # Function to test map creation and map-of-maps 312 + # Parameters: 313 + # $1: bpftool path 314 + # $2: BPF_DIR 315 + test_map_creation_and_map_of_maps() { 316 + local bpftool_path="$1" 317 + local bpf_dir="$2" 318 + local outer_map_name="outer_map_tt" 319 + local inner_map_name="inner_map_tt" 320 + 321 + "$bpftool_path" map create "$bpf_dir/$inner_map_name" type array key 4 \ 322 + value 4 entries 4 name "$inner_map_name" 323 + if [ ! -f "$bpf_dir/$inner_map_name" ]; then 324 + echo " Failed to create inner map file at $bpf_dir/$outer_map_name" 325 + return 1 326 + fi 327 + 328 + "$bpftool_path" map create "$bpf_dir/$outer_map_name" type hash_of_maps \ 329 + key 4 value 4 entries 2 name "$outer_map_name" inner_map name "$inner_map_name" 330 + if [ ! -f "$bpf_dir/$outer_map_name" ]; then 331 + echo " Failed to create outer map file at $bpf_dir/$outer_map_name" 332 + return 1 333 + fi 334 + 335 + # Add entries to the outer map by name and by pinned path. 336 + "$bpftool_path" map update pinned "$bpf_dir/$outer_map_name" key 0 0 0 0 \ 337 + value pinned "$bpf_dir/$inner_map_name" 338 + "$bpftool_path" map update name "$outer_map_name" key 1 0 0 0 value \ 339 + name "$inner_map_name" 340 + 341 + # The outer map should be full by now. 342 + # The following map update command is expected to fail. 343 + if "$bpftool_path" map update name "$outer_map_name" key 2 0 0 0 value name \ 344 + "$inner_map_name" 2>/dev/null; then 345 + echo " Update for $outer_map_name succeeded but should have failed" 346 + exit 1 347 + fi 348 + } 349 + 350 + # Function to test map access with the btf list command 351 + # Parameters: 352 + # $1: bpftool path 353 + test_map_access_with_btf_list() { 354 + local bpftool_path="$1" 355 + 356 + # The btf list command iterates over maps for 357 + # loaded BPF programs. 358 + if ! "$bpftool_path" btf list 1>/dev/null; then 359 + echo " Failed to access btf data" 360 + exit 1 361 + fi 362 + } 363 + 364 + set -eu 365 + 366 + trap cleanup_skip EXIT 367 + 368 + check_root_privileges 369 + 370 + verify_bpftool_path "$BPFTOOL_PATH" 371 + 372 + verify_btf_support 373 + 374 + trap cleanup EXIT 375 + 376 + # Load and attach the BPF programs to control maps access. 377 + "$BPFTOOL_PATH" prog loadall "$BPF_FILE_PATH" "$BPF_DIR" autoattach 378 + 379 + initialize_map_entries "$PROTECTED_MAP_NAME" "$BPFTOOL_PATH" 380 + initialize_map_entries "$NOT_PROTECTED_MAP_NAME" "$BPFTOOL_PATH" 381 + 382 + # Activate the map protection mechanism. Protection status is controlled 383 + # by a value stored in the prot_status_map at index 0. 384 + "$BPFTOOL_PATH" map update name prot_status_map key 0 0 0 0 value 1 0 0 0 385 + 386 + # Test protected map (write should fail). 387 + test_map_access "$PROTECTED_MAP_NAME" "$BPFTOOL_PATH" "$BPF_DIR" "false" \ 388 + "$BPF_ITER_FILE_PATH" 389 + 390 + # Test not protected map (write should succeed). 391 + test_map_access "$NOT_PROTECTED_MAP_NAME" "$BPFTOOL_PATH" "$BPF_DIR" "true" \ 392 + "$BPF_ITER_FILE_PATH" 393 + 394 + test_map_creation_and_map_of_maps "$BPFTOOL_PATH" "$BPF_DIR" 395 + 396 + test_map_access_with_btf_list "$BPFTOOL_PATH" 397 + 398 + exit 0