1 // SPDX-License-Identifier: GPL-2.0 2 /* Copyright (c) 2026 Christian Brauner <brauner@kernel.org> */ 3 4 /* 5 * Test BPF LSM block device integrity hooks with dm-verity. 6 * 7 * Creates a dm-verity device over loopback, which triggers 8 * security_bdev_setintegrity() during verity_preresume(). 9 * Verifies that the BPF program correctly tracks the integrity 10 * metadata in its hashmap. 11 */ 12 13 #define _GNU_SOURCE 14 #include <test_progs.h> 15 #include <fcntl.h> 16 #include <stdio.h> 17 #include <stdlib.h> 18 #include <string.h> 19 #include <sys/stat.h> 20 #include <sys/types.h> 21 #include <unistd.h> 22 #include "lsm_bdev.skel.h" 23 24 /* Must match the definition in progs/lsm_bdev.c. */ 25 struct verity_info { 26 __u8 has_roothash; 27 __u8 sig_valid; 28 __u32 setintegrity_cnt; 29 }; 30 31 #define DATA_SIZE_MB 8 32 #define HASH_SIZE_MB 1 33 #define DM_NAME "bpf_test_verity" 34 #define DM_DEV_PATH "/dev/mapper/" DM_NAME 35 36 /* Run a command and optionally capture the first line of stdout. */ 37 static int run_cmd(const char *cmd, char *out, size_t out_sz) 38 { 39 FILE *fp; 40 int ret; 41 42 fp = popen(cmd, "r"); 43 if (!fp) 44 return -1; 45 46 if (out && out_sz > 0) { 47 if (!fgets(out, out_sz, fp)) 48 out[0] = '\0'; 49 /* strip trailing newline */ 50 out[strcspn(out, "\n")] = '\0'; 51 } 52 53 ret = pclose(fp); 54 return WIFEXITED(ret) ? WEXITSTATUS(ret) : -1; 55 } 56 57 static bool has_prerequisites(void) 58 { 59 if (getuid() != 0) { 60 printf("SKIP: must be root\n"); 61 return false; 62 } 63 64 if (run_cmd("modprobe loop 2>/dev/null", NULL, 0) && 65 run_cmd("ls /dev/loop-control 2>/dev/null", NULL, 0)) { 66 printf("SKIP: no loop device support\n"); 67 return false; 68 } 69 70 if (run_cmd("modprobe dm-verity 2>/dev/null", NULL, 0) && 71 run_cmd("dmsetup targets 2>/dev/null | grep -q verity", NULL, 0)) { 72 printf("SKIP: dm-verity module not available\n"); 73 return false; 74 } 75 76 if (run_cmd("which veritysetup >/dev/null 2>&1", NULL, 0)) { 77 printf("SKIP: veritysetup not found\n"); 78 return false; 79 } 80 81 return true; 82 } 83 84 void test_lsm_bdev(void) 85 { 86 char data_img[] = "/tmp/bpf_verity_data_XXXXXX"; 87 char hash_img[] = "/tmp/bpf_verity_hash_XXXXXX"; 88 char data_loop[64] = {}; 89 char hash_loop[64] = {}; 90 char roothash[256] = {}; 91 char cmd[512]; 92 int data_fd = -1, hash_fd = -1; 93 struct lsm_bdev *skel = NULL; 94 struct verity_info val; 95 struct stat st; 96 __u32 dev_key; 97 int err; 98 99 if (!has_prerequisites()) { 100 test__skip(); 101 return; 102 } 103 104 /* Clean up any stale device from a previous crashed run. */ 105 snprintf(cmd, sizeof(cmd), "dmsetup remove %s 2>/dev/null", DM_NAME); 106 run_cmd(cmd, NULL, 0); 107 108 /* Create temporary image files. */ 109 data_fd = mkstemp(data_img); 110 if (!ASSERT_OK_FD(data_fd, "mkstemp data")) 111 return; 112 113 hash_fd = mkstemp(hash_img); 114 if (!ASSERT_OK_FD(hash_fd, "mkstemp hash")) 115 goto cleanup; 116 117 if (!ASSERT_OK(ftruncate(data_fd, DATA_SIZE_MB * 1024 * 1024), 118 "truncate data")) 119 goto cleanup; 120 121 if (!ASSERT_OK(ftruncate(hash_fd, HASH_SIZE_MB * 1024 * 1024), 122 "truncate hash")) 123 goto cleanup; 124 125 close(data_fd); 126 data_fd = -1; 127 close(hash_fd); 128 hash_fd = -1; 129 130 /* Set up loop devices. */ 131 snprintf(cmd, sizeof(cmd), 132 "losetup --find --show %s 2>/dev/null", data_img); 133 if (!ASSERT_OK(run_cmd(cmd, data_loop, sizeof(data_loop)), 134 "losetup data")) 135 goto teardown; 136 137 snprintf(cmd, sizeof(cmd), 138 "losetup --find --show %s 2>/dev/null", hash_img); 139 if (!ASSERT_OK(run_cmd(cmd, hash_loop, sizeof(hash_loop)), 140 "losetup hash")) 141 goto teardown; 142 143 /* Format the dm-verity device and capture the root hash. */ 144 snprintf(cmd, sizeof(cmd), 145 "veritysetup format %s %s 2>/dev/null | " 146 "grep -i 'root hash' | awk '{print $NF}'", 147 data_loop, hash_loop); 148 if (!ASSERT_OK(run_cmd(cmd, roothash, sizeof(roothash)), 149 "veritysetup format")) 150 goto teardown; 151 152 if (!ASSERT_GT((int)strlen(roothash), 0, "roothash not empty")) 153 goto teardown; 154 155 /* Load and attach BPF program before activating dm-verity. */ 156 skel = lsm_bdev__open_and_load(); 157 if (!ASSERT_OK_PTR(skel, "skel open_and_load")) 158 goto teardown; 159 160 err = lsm_bdev__attach(skel); 161 if (!ASSERT_OK(err, "skel attach")) 162 goto teardown; 163 164 /* Activate dm-verity — triggers verity_preresume() hooks. */ 165 snprintf(cmd, sizeof(cmd), 166 "veritysetup open %s %s %s %s 2>/dev/null", 167 data_loop, DM_NAME, hash_loop, roothash); 168 if (!ASSERT_OK(run_cmd(cmd, NULL, 0), "veritysetup open")) 169 goto teardown; 170 171 /* Get the dm device's dev_t. */ 172 if (!ASSERT_OK(stat(DM_DEV_PATH, &st), "stat dm dev")) 173 goto remove_dm; 174 175 dev_key = (__u32)st.st_rdev; 176 177 /* Look up the device in the BPF map and verify. */ 178 err = bpf_map__lookup_elem(skel->maps.verity_devices, 179 &dev_key, sizeof(dev_key), 180 &val, sizeof(val), 0); 181 if (!ASSERT_OK(err, "map lookup")) 182 goto remove_dm; 183 184 ASSERT_EQ(val.has_roothash, 1, "has_roothash"); 185 ASSERT_EQ(val.sig_valid, 0, "sig_valid (unsigned)"); 186 /* 187 * verity_preresume() always calls security_bdev_setintegrity() 188 * for the roothash. The signature-validity call only happens 189 * when CONFIG_DM_VERITY_VERIFY_ROOTHASH_SIG is enabled. 190 */ 191 ASSERT_GE(val.setintegrity_cnt, 1, "setintegrity_cnt min"); 192 ASSERT_LE(val.setintegrity_cnt, 2, "setintegrity_cnt max"); 193 194 /* Verify that the alloc hook fired at least once. */ 195 ASSERT_GT(skel->bss->alloc_count, 0, "alloc_count"); 196 197 remove_dm: 198 snprintf(cmd, sizeof(cmd), "dmsetup remove %s 2>/dev/null", DM_NAME); 199 run_cmd(cmd, NULL, 0); 200 201 teardown: 202 if (data_loop[0]) { 203 snprintf(cmd, sizeof(cmd), "losetup -d %s 2>/dev/null", 204 data_loop); 205 run_cmd(cmd, NULL, 0); 206 } 207 if (hash_loop[0]) { 208 snprintf(cmd, sizeof(cmd), "losetup -d %s 2>/dev/null", 209 hash_loop); 210 run_cmd(cmd, NULL, 0); 211 } 212 213 cleanup: 214 lsm_bdev__destroy(skel); 215 if (data_fd >= 0) 216 close(data_fd); 217 if (hash_fd >= 0) 218 close(hash_fd); 219 unlink(data_img); 220 unlink(hash_img); 221 } 222