| File: | .build-ci/../keysd/tests/test-kmip-client.c |
| Warning: | line 195, column 13 Potential memory leak |
Press '?' to see keyboard shortcuts
Keyboard shortcuts:
| 1 | // SPDX-License-Identifier: GPL-2.0-or-later | |||
| 2 | /* | |||
| 3 | * This file is part of nvme-cli. | |||
| 4 | * Copyright (c) 2026 Dell Technologies Inc. or its subsidiaries. | |||
| 5 | * | |||
| 6 | * Authors: Martin Belanger <martin.belanger@dell.com> | |||
| 7 | */ | |||
| 8 | ||||
| 9 | #include <errno(*__errno_location ()).h> | |||
| 10 | #include <stdbool.h> | |||
| 11 | #include <stdio.h> | |||
| 12 | #include <stdlib.h> | |||
| 13 | #include <string.h> | |||
| 14 | #include <unistd.h> | |||
| 15 | ||||
| 16 | #include <systemd/sd-event.h> | |||
| 17 | #include <systemd/sd-json.h> | |||
| 18 | #include <systemd/sd-varlink.h> | |||
| 19 | ||||
| 20 | #include <shared/io-util.h> | |||
| 21 | ||||
| 22 | #include "config.h" | |||
| 23 | #include "import.h" | |||
| 24 | #include "kmip-client.h" | |||
| 25 | #include "kmip-varlink.h" | |||
| 26 | ||||
| 27 | #define MAX_CALLS16 16 | |||
| 28 | ||||
| 29 | /* | |||
| 30 | * A fake nvme-keysd-kmip. It answers GetKey after a short delay, so that | |||
| 31 | * calls overlap. The key of "name" is "PSK-name". It answers from server | |||
| 32 | * 1 when the KMS has two servers. | |||
| 33 | * | |||
| 34 | * "fail-once" fails the first call with KmsFailed. | |||
| 35 | */ | |||
| 36 | struct fake_call { | |||
| 37 | char kms[32]; | |||
| 38 | char name[32]; | |||
| 39 | uint64_t server; | |||
| 40 | }; | |||
| 41 | ||||
| 42 | static struct fake { | |||
| 43 | sd_event *event; | |||
| 44 | struct fake_call calls[MAX_CALLS16]; | |||
| 45 | int nr_calls; | |||
| 46 | int in_flight; | |||
| 47 | int max_in_flight; | |||
| 48 | int in_flight_kms[2]; // for KMS "a" and "b" | |||
| 49 | int max_in_flight_kms; | |||
| 50 | } fake; | |||
| 51 | ||||
| 52 | // Imports, done by the client. | |||
| 53 | static struct { | |||
| 54 | int nr; | |||
| 55 | int expected; // stop the loop after this many imports | |||
| 56 | int fail_left; // imports to fail before one succeeds | |||
| 57 | bool_Bool bad_key; | |||
| 58 | } imports; | |||
| 59 | ||||
| 60 | static struct kmip_client *client; | |||
| 61 | static sd_varlink_server *server; | |||
| 62 | ||||
| 63 | static bool_Bool check(bool_Bool cond, const char *what) | |||
| 64 | { | |||
| 65 | printf(" - %s [%s]\n", what, cond ? "PASS" : "FAIL"); | |||
| 66 | return cond; | |||
| 67 | } | |||
| 68 | ||||
| 69 | struct pending_reply { | |||
| 70 | sd_varlink *link; | |||
| 71 | struct fake_call *call; | |||
| 72 | }; | |||
| 73 | ||||
| 74 | static int kms_index(const char *kms) | |||
| 75 | { | |||
| 76 | return strcmp(kms, "a") ? 1 : 0; | |||
| 77 | } | |||
| 78 | ||||
| 79 | static int send_reply(sd_event_source *s, uint64_t usec, void *userdata) | |||
| 80 | { | |||
| 81 | struct pending_reply *pr = userdata; | |||
| 82 | struct fake_call *call = pr->call; | |||
| 83 | char key[48]; | |||
| 84 | ||||
| 85 | fake.in_flight--; | |||
| 86 | fake.in_flight_kms[kms_index(call->kms)]--; | |||
| 87 | ||||
| 88 | if (!strcmp(call->name, "fail-once") && | |||
| 89 | call == &fake.calls[0]) | |||
| 90 | sd_varlink_error(pr->link, KMIP_VARLINK_KMS_FAILED"io.nvme.keysd.Kmip" ".KmsFailed", NULL((void*)0)); | |||
| 91 | else { | |||
| 92 | snprintf(key, sizeof(key), "PSK-%s", call->name); | |||
| 93 | sd_varlink_replybo(pr->link,sd_varlink_replyb((pr->link), _SD_JSON_BUILD_OBJECT_BEGIN, _SD_JSON_BUILD_PAIR, (const char*) { "key" }, _SD_JSON_BUILD_STRING , (const char*) { key }, _SD_JSON_BUILD_PAIR, (const char*) { "server" }, _SD_JSON_BUILD_UNSIGNED, (uint64_t) { kms_index( call->kms) }, _SD_JSON_BUILD_OBJECT_END) | |||
| 94 | SD_JSON_BUILD_PAIR_STRING("key", key),sd_varlink_replyb((pr->link), _SD_JSON_BUILD_OBJECT_BEGIN, _SD_JSON_BUILD_PAIR, (const char*) { "key" }, _SD_JSON_BUILD_STRING , (const char*) { key }, _SD_JSON_BUILD_PAIR, (const char*) { "server" }, _SD_JSON_BUILD_UNSIGNED, (uint64_t) { kms_index( call->kms) }, _SD_JSON_BUILD_OBJECT_END) | |||
| 95 | SD_JSON_BUILD_PAIR_UNSIGNED("server",sd_varlink_replyb((pr->link), _SD_JSON_BUILD_OBJECT_BEGIN, _SD_JSON_BUILD_PAIR, (const char*) { "key" }, _SD_JSON_BUILD_STRING , (const char*) { key }, _SD_JSON_BUILD_PAIR, (const char*) { "server" }, _SD_JSON_BUILD_UNSIGNED, (uint64_t) { kms_index( call->kms) }, _SD_JSON_BUILD_OBJECT_END) | |||
| 96 | kms_index(call->kms)))sd_varlink_replyb((pr->link), _SD_JSON_BUILD_OBJECT_BEGIN, _SD_JSON_BUILD_PAIR, (const char*) { "key" }, _SD_JSON_BUILD_STRING , (const char*) { key }, _SD_JSON_BUILD_PAIR, (const char*) { "server" }, _SD_JSON_BUILD_UNSIGNED, (uint64_t) { kms_index( call->kms) }, _SD_JSON_BUILD_OBJECT_END); | |||
| 97 | } | |||
| 98 | ||||
| 99 | sd_varlink_unref(pr->link); | |||
| 100 | free(pr); | |||
| 101 | sd_event_source_disable_unref(s); | |||
| 102 | ||||
| 103 | return 0; | |||
| 104 | } | |||
| 105 | ||||
| 106 | static int fake_get_key(sd_varlink *link, sd_json_variant *params, | |||
| 107 | sd_varlink_method_flags_t flags, void *userdata) | |||
| 108 | { | |||
| 109 | struct pending_reply *pr; | |||
| 110 | struct fake_call *call; | |||
| 111 | sd_json_variant *v; | |||
| 112 | uint64_t now; | |||
| 113 | int i; | |||
| 114 | ||||
| 115 | if (fake.nr_calls == MAX_CALLS16) | |||
| 116 | return sd_varlink_error(link, KMIP_VARLINK_KMS_FAILED"io.nvme.keysd.Kmip" ".KmsFailed", NULL((void*)0)); | |||
| 117 | call = &fake.calls[fake.nr_calls++]; | |||
| 118 | ||||
| 119 | v = sd_json_variant_by_key(params, "kms"); | |||
| 120 | snprintf(call->kms, sizeof(call->kms), "%s", | |||
| 121 | v ? sd_json_variant_string(v) : ""); | |||
| 122 | v = sd_json_variant_by_key(params, "name"); | |||
| 123 | snprintf(call->name, sizeof(call->name), "%s", | |||
| 124 | v ? sd_json_variant_string(v) : ""); | |||
| 125 | v = sd_json_variant_by_key(params, "server"); | |||
| 126 | call->server = v ? sd_json_variant_unsigned(v) : 99; | |||
| 127 | ||||
| 128 | fake.in_flight++; | |||
| 129 | if (fake.in_flight > fake.max_in_flight) | |||
| 130 | fake.max_in_flight = fake.in_flight; | |||
| 131 | i = kms_index(call->kms); | |||
| 132 | fake.in_flight_kms[i]++; | |||
| 133 | if (fake.in_flight_kms[i] > fake.max_in_flight_kms) | |||
| 134 | fake.max_in_flight_kms = fake.in_flight_kms[i]; | |||
| 135 | ||||
| 136 | pr = calloc(1, sizeof(*pr)); | |||
| 137 | if (!pr) | |||
| 138 | return -ENOMEM12; | |||
| 139 | pr->link = sd_varlink_ref(link); | |||
| 140 | pr->call = call; | |||
| 141 | ||||
| 142 | sd_event_now(fake.event, CLOCK_MONOTONIC1, &now); | |||
| 143 | ||||
| 144 | return sd_event_add_time(fake.event, NULL((void*)0), CLOCK_MONOTONIC1, | |||
| 145 | now + 20000, 0, send_reply, pr); | |||
| 146 | } | |||
| 147 | ||||
| 148 | static int test_import(const struct kmip_job *job, const char *encoded, | |||
| 149 | void *arg) | |||
| 150 | { | |||
| 151 | char key[48]; | |||
| 152 | ||||
| 153 | snprintf(key, sizeof(key), "PSK-%s", job->name); | |||
| 154 | if (strcmp(encoded, key)) | |||
| 155 | imports.bad_key = true1; | |||
| 156 | ||||
| 157 | if (imports.fail_left) { | |||
| 158 | imports.fail_left--; | |||
| 159 | return -EIO5; | |||
| 160 | } | |||
| 161 | imports.nr++; | |||
| 162 | ||||
| 163 | if (imports.nr == imports.expected) | |||
| 164 | sd_event_exit(fake.event, 0); | |||
| 165 | ||||
| 166 | return 0; | |||
| 167 | } | |||
| 168 | ||||
| 169 | /* Load nvme-keysd.conf with KMS "a" (one server) and "b" (two servers). */ | |||
| 170 | static struct keysd_config *load_cfg(void) | |||
| 171 | { | |||
| 172 | static const char text[] = | |||
| 173 | "[kmip:a]\nserver = 192.0.2.1\n" | |||
| 174 | "[kmip:b]\nserver = 192.0.2.2\nserver = 192.0.2.3\n"; | |||
| 175 | char path[] = "/tmp/nvme-keysd-client-XXXXXX"; | |||
| 176 | struct keysd_config *cfg; | |||
| 177 | int fd; | |||
| 178 | ||||
| 179 | fd = mkstemp(path); | |||
| 180 | if (fd < 0 || shr_write_all(fd, text, sizeof(text) - 1)) { | |||
| 181 | fprintf(stderrstderr, "cannot write the config\n"); | |||
| 182 | exit(EXIT_FAILURE1); | |||
| 183 | } | |||
| 184 | close(fd); | |||
| 185 | cfg = config_load(path); | |||
| 186 | unlink(path); | |||
| 187 | ||||
| 188 | return cfg; | |||
| 189 | } | |||
| 190 | ||||
| 191 | static void set_job(struct kmip_job *job, const struct keysd_config *cfg, | |||
| 192 | const char *kms, const char *name) | |||
| 193 | { | |||
| 194 | memset(job, 0, sizeof(*job)); | |||
| 195 | job->kms = config_find_kmip(cfg, kms); | |||
| ||||
| 196 | job->source = strdup("test.conf"); | |||
| 197 | job->subsysnqn = strdup("nqn.test"); | |||
| 198 | job->name = strdup(name); | |||
| 199 | } | |||
| 200 | ||||
| 201 | static void free_jobs(struct kmip_job *jobs, size_t n) | |||
| 202 | { | |||
| 203 | size_t i; | |||
| 204 | ||||
| 205 | for (i = 0; i < n; i++) { | |||
| 206 | free(jobs[i].source); | |||
| 207 | free(jobs[i].subsysnqn); | |||
| 208 | free(jobs[i].name); | |||
| 209 | } | |||
| 210 | } | |||
| 211 | ||||
| 212 | /* | |||
| 213 | * Run the client on @jobs against the fake helper, until every key is | |||
| 214 | * imported or @limit_usec passes. | |||
| 215 | */ | |||
| 216 | static void run(const char *address, const struct kmip_job *jobs, size_t n, | |||
| 217 | uint64_t limit_usec) | |||
| 218 | { | |||
| 219 | const struct kmip_client_cfg cfg = { | |||
| 220 | .address = address, | |||
| 221 | .retry_usec = 10000, | |||
| 222 | .retry_max_usec = 40000, | |||
| 223 | .import = test_import, | |||
| 224 | }; | |||
| 225 | sd_event_source *limit; | |||
| 226 | uint64_t now; | |||
| 227 | ||||
| 228 | memset(&fake.calls, 0, sizeof(fake.calls)); | |||
| 229 | fake.nr_calls = 0; | |||
| 230 | fake.max_in_flight = 0; | |||
| 231 | fake.max_in_flight_kms = 0; | |||
| 232 | imports.nr = 0; | |||
| 233 | imports.expected = n; | |||
| 234 | imports.bad_key = false0; | |||
| 235 | ||||
| 236 | // A loop cannot run again after it ends: use a new one. | |||
| 237 | if (sd_event_new(&fake.event) < 0 || | |||
| 238 | sd_varlink_server_attach_event(server, fake.event, 0) < 0) { | |||
| 239 | fprintf(stderrstderr, "event loop setup failed\n"); | |||
| 240 | exit(EXIT_FAILURE1); | |||
| 241 | } | |||
| 242 | ||||
| 243 | // A NULL callback ends the loop. | |||
| 244 | sd_event_now(fake.event, CLOCK_MONOTONIC1, &now); | |||
| 245 | if (sd_event_add_time(fake.event, &limit, CLOCK_MONOTONIC1, | |||
| 246 | now + limit_usec, 0, NULL((void*)0), NULL((void*)0)) < 0) { | |||
| 247 | fprintf(stderrstderr, "sd_event_add_time failed\n"); | |||
| 248 | exit(EXIT_FAILURE1); | |||
| 249 | } | |||
| 250 | ||||
| 251 | if (kmip_client_new(fake.event, &cfg, jobs, n, &client) < 0) { | |||
| 252 | fprintf(stderrstderr, "kmip_client_new failed\n"); | |||
| 253 | exit(EXIT_FAILURE1); | |||
| 254 | } | |||
| 255 | sd_event_loop(fake.event); | |||
| 256 | kmip_client_free(client); | |||
| 257 | client = NULL((void*)0); | |||
| 258 | sd_event_source_disable_unref(limit); | |||
| 259 | sd_varlink_server_detach_event(server); | |||
| 260 | fake.event = sd_event_unref(fake.event); | |||
| 261 | } | |||
| 262 | ||||
| 263 | static bool_Bool test_parallel(const char *address, | |||
| 264 | const struct keysd_config *cfg) | |||
| 265 | { | |||
| 266 | struct kmip_job jobs[4]; | |||
| 267 | bool_Bool pass = true1; | |||
| 268 | int i, b_calls = 0; | |||
| 269 | ||||
| 270 | printf("test_parallel:\n"); | |||
| 271 | ||||
| 272 | set_job(&jobs[0], cfg, "a", "k1"); | |||
| ||||
| 273 | set_job(&jobs[1], cfg, "a", "k2"); | |||
| 274 | set_job(&jobs[2], cfg, "b", "k3"); | |||
| 275 | set_job(&jobs[3], cfg, "b", "k4"); | |||
| 276 | run(address, jobs, 4, 2000000); | |||
| 277 | ||||
| 278 | pass &= check(imports.nr == 4 && !imports.bad_key, | |||
| 279 | "all keys imported"); | |||
| 280 | pass &= check(fake.max_in_flight_kms == 1, | |||
| 281 | "one call at a time per KMS"); | |||
| 282 | pass &= check(fake.max_in_flight == 2, "KMSes asked in parallel"); | |||
| 283 | ||||
| 284 | for (i = 0; i < fake.nr_calls; i++) { | |||
| 285 | if (strcmp(fake.calls[i].kms, "b")) | |||
| 286 | continue; | |||
| 287 | if (b_calls) | |||
| 288 | pass &= check(fake.calls[i].server == 1, | |||
| 289 | "next call asks the last server first"); | |||
| 290 | else | |||
| 291 | pass &= check(fake.calls[i].server == 0, | |||
| 292 | "first call asks server 0 first"); | |||
| 293 | b_calls++; | |||
| 294 | } | |||
| 295 | free_jobs(jobs, 4); | |||
| 296 | ||||
| 297 | return pass; | |||
| 298 | } | |||
| 299 | ||||
| 300 | static bool_Bool test_retry(const char *address, const struct keysd_config *cfg) | |||
| 301 | { | |||
| 302 | struct kmip_job job; | |||
| 303 | bool_Bool pass = true1; | |||
| 304 | ||||
| 305 | printf("test_retry:\n"); | |||
| 306 | ||||
| 307 | set_job(&job, cfg, "a", "fail-once"); | |||
| 308 | run(address, &job, 1, 2000000); | |||
| 309 | pass &= check(imports.nr == 1 && fake.nr_calls == 2, | |||
| 310 | "KMS error retried"); | |||
| 311 | free_jobs(&job, 1); | |||
| 312 | ||||
| 313 | set_job(&job, cfg, "a", "k1"); | |||
| 314 | imports.fail_left = 2; | |||
| 315 | run(address, &job, 1, 2000000); | |||
| 316 | pass &= check(imports.nr == 1 && fake.nr_calls == 3, | |||
| 317 | "import error retried"); | |||
| 318 | free_jobs(&job, 1); | |||
| 319 | ||||
| 320 | set_job(&job, cfg, "a", "k1"); | |||
| 321 | run("/nonexistent/kmip", &job, 1, 100000); | |||
| 322 | pass &= check(imports.nr == 0, "no helper: key stays pending"); | |||
| 323 | free_jobs(&job, 1); | |||
| 324 | ||||
| 325 | return pass; | |||
| 326 | } | |||
| 327 | ||||
| 328 | int main(void) | |||
| 329 | { | |||
| 330 | char dir[] = "/tmp/test-kmip-client-XXXXXX", address[108], *abs; | |||
| 331 | struct keysd_config *cfg; | |||
| 332 | bool_Bool pass = true1; | |||
| 333 | ||||
| 334 | cfg = load_cfg(); | |||
| 335 | if (!cfg || cfg->nr_kmip != 2 || !mkdtemp(dir) || | |||
| 336 | sd_varlink_server_new(&server, 0) < 0) { | |||
| 337 | fprintf(stderrstderr, "test setup failed\n"); | |||
| 338 | exit(EXIT_FAILURE1); | |||
| 339 | } | |||
| 340 | // A socket address must be absolute. | |||
| 341 | abs = realpath(dir, NULL((void*)0)); | |||
| 342 | if (!abs || snprintf(address, sizeof(address), "%s/kmip", abs) >= | |||
| 343 | (int)sizeof(address)) { | |||
| 344 | fprintf(stderrstderr, "socket path too long\n"); | |||
| 345 | exit(EXIT_FAILURE1); | |||
| 346 | } | |||
| 347 | free(abs); | |||
| 348 | ||||
| 349 | if (sd_varlink_server_bind_method(server, KMIP_VARLINK_GET_KEY"io.nvme.keysd.Kmip" ".GetKey", | |||
| 350 | fake_get_key) < 0 || | |||
| 351 | sd_varlink_server_listen_address(server, address, 0600) < 0) { | |||
| 352 | fprintf(stderrstderr, "fake helper setup failed\n"); | |||
| 353 | exit(EXIT_FAILURE1); | |||
| 354 | } | |||
| 355 | ||||
| 356 | pass &= test_parallel(address, cfg); | |||
| 357 | pass &= test_retry(address, cfg); | |||
| 358 | ||||
| 359 | sd_varlink_server_unref(server); | |||
| 360 | unlink(address); | |||
| 361 | rmdir(dir); | |||
| 362 | config_free(cfg); | |||
| 363 | ||||
| 364 | fflush(stdoutstdout); | |||
| 365 | exit(pass ? EXIT_SUCCESS0 : EXIT_FAILURE1); | |||
| 366 | } |