| File: | .build-ci/../libnvme/src/nvme/exclusion.c |
| Warning: | line 900, column 12 Potential leak of memory pointed to by 'copy' |
Press '?' to see keyboard shortcuts
Keyboard shortcuts:
| 1 | // SPDX-License-Identifier: LGPL-2.1-or-later | |||
| 2 | /* | |||
| 3 | * This file is part of libnvme. | |||
| 4 | * Copyright (c) 2026 Dell Technologies Inc. or its subsidiaries. | |||
| 5 | * | |||
| 6 | * Authors: Martin Belanger <martin.belanger@dell.com> | |||
| 7 | */ | |||
| 8 | ||||
| 9 | /* | |||
| 10 | * System-wide NVMe-oF exclusion list. | |||
| 11 | * | |||
| 12 | * Storage: a hand-edited main file SYSCONFDIR/nvme/exclusions.conf plus managed | |||
| 13 | * drop-in lists under SYSCONFDIR/nvme/exclusions.conf.d/<name>.conf. Matching | |||
| 14 | * consults the main file and every drop-in. | |||
| 15 | * Format: an "[exclusions]" INI section holding one "exclusion = key=val;..." | |||
| 16 | * line per entry; # lines are comments. Other sections are reserved for | |||
| 17 | * future use and their content is ignored. | |||
| 18 | */ | |||
| 19 | ||||
| 20 | #include <dirent.h> | |||
| 21 | #include <errno(*__errno_location ()).h> | |||
| 22 | #include <fcntl.h> | |||
| 23 | #include <limits.h> | |||
| 24 | #include <stdint.h> | |||
| 25 | #include <stdio.h> | |||
| 26 | #include <stdlib.h> | |||
| 27 | #include <string.h> | |||
| 28 | #include <sys/file.h> | |||
| 29 | #include <sys/stat.h> | |||
| 30 | #include <unistd.h> | |||
| 31 | ||||
| 32 | #include <compiler-attributes.h> | |||
| 33 | #include <fs-util.h> | |||
| 34 | #include <hash-util.h> | |||
| 35 | #include <io-util.h> | |||
| 36 | #include <string-util.h> | |||
| 37 | ||||
| 38 | #include "cleanup.h" | |||
| 39 | #include "exclusion.h" | |||
| 40 | #include "lib.h" | |||
| 41 | #include "nvme/accessors-fabrics.h" | |||
| 42 | #include "nvme/tid.h" | |||
| 43 | #include "private.h" | |||
| 44 | #include "private-fabrics.h" | |||
| 45 | ||||
| 46 | #define EXCL_BASE_DEFAULT"/usr/local/etc" "/nvme" SYSCONFDIR"/usr/local/etc" "/nvme" | |||
| 47 | #define EXCL_MAIN_NAME"exclusions.conf" "exclusions.conf" | |||
| 48 | #define EXCL_DROPIN_NAME"exclusions.conf.d" "exclusions.conf.d" | |||
| 49 | #define EXCL_MAIN_PATH"/usr/local/etc" "/nvme" "/" "exclusions.conf" EXCL_BASE_DEFAULT"/usr/local/etc" "/nvme" "/" EXCL_MAIN_NAME"exclusions.conf" | |||
| 50 | #define EXCL_DROPIN_PATH"/usr/local/etc" "/nvme" "/" "exclusions.conf.d" EXCL_BASE_DEFAULT"/usr/local/etc" "/nvme" "/" EXCL_DROPIN_NAME"exclusions.conf.d" | |||
| 51 | #define EXCL_SECTION"exclusions" "exclusions" | |||
| 52 | #define EXCL_LINE_KEY"exclusion" "exclusion" | |||
| 53 | #define EXCL_LINE_MAX4096 4096 | |||
| 54 | #define EXCL_FILE_MAX(1 * 1024 * 1024) (1 * 1024 * 1024) /* 1 MiB cap on one list file */ | |||
| 55 | ||||
| 56 | /* Header written whenever a list file is first created (via create or add). */ | |||
| 57 | #define EXCL_HEADER_FMT"# NVMe-oF exclusion list: %s\n" "# Format: exclusion = key=val;key=val\n" "# Keys: transport, traddr, trsvcid, nqn, host-traddr, host-iface, hostnqn, hostid\n" "\n" "[" "exclusions" "]\n" \ | |||
| 58 | "# NVMe-oF exclusion list: %s\n" \ | |||
| 59 | "# Format: exclusion = key=val;key=val\n" \ | |||
| 60 | "# Keys: transport, traddr, trsvcid, nqn, host-traddr, host-iface, hostnqn, hostid\n" \ | |||
| 61 | "\n" \ | |||
| 62 | "[" EXCL_SECTION"exclusions" "]\n" | |||
| 63 | ||||
| 64 | /* | |||
| 65 | * Path helpers. The normal (production) paths are fixed compile-time literals, | |||
| 66 | * returned directly. A ctx test sandbox (libnvme_set_test_base_dir()) reroots | |||
| 67 | * them under a throwaway /tmp directory; the path is built once into a static | |||
| 68 | * and cached. Caching in a process-wide static is safe even though the | |||
| 69 | * sandbox is a per-ctx property: it is a single-process test feature, so a | |||
| 70 | * process is either all-production or all-test and the first call fixes the | |||
| 71 | * right value for the whole run. Returning fixed paths this way keeps the | |||
| 72 | * callers free of local path buffers; only excl_path() for a *named* drop-in | |||
| 73 | * (whose path varies with @name) fills a caller buffer. | |||
| 74 | */ | |||
| 75 | static const char *excl_base(struct libnvme_global_ctx *ctx) | |||
| 76 | { | |||
| 77 | return ctx->test_base_dir ? ctx->test_base_dir : EXCL_BASE_DEFAULT"/usr/local/etc" "/nvme"; | |||
| 78 | } | |||
| 79 | ||||
| 80 | /* | |||
| 81 | * Return a fixed exclusion path: the compile-time literal @prod_path in | |||
| 82 | * production, or <test_base_dir>/@name under a sandbox (built into @buf and | |||
| 83 | * remembered in @cache). See the note above on why the static cache is safe. | |||
| 84 | */ | |||
| 85 | static const char *excl_fixed_path(struct libnvme_global_ctx *ctx, | |||
| 86 | const char *prod_path, const char *name, | |||
| 87 | const char **cache, char *buf, size_t len) | |||
| 88 | { | |||
| 89 | int n; | |||
| 90 | ||||
| 91 | if (*cache) | |||
| 92 | return *cache; | |||
| 93 | if (!ctx->test_base_dir) { | |||
| 94 | *cache = prod_path; | |||
| 95 | } else { | |||
| 96 | n = snprintf(buf, len, "%s/%s", ctx->test_base_dir, name); | |||
| 97 | *cache = (n > 0 && (size_t)n < len) ? buf : NULL((void*)0); | |||
| 98 | } | |||
| 99 | return *cache; | |||
| 100 | } | |||
| 101 | ||||
| 102 | /* Directory holding the managed drop-in lists (<base>/exclusions.conf.d). */ | |||
| 103 | static const char *excl_dropin_dir(struct libnvme_global_ctx *ctx) | |||
| 104 | { | |||
| 105 | static const char *cache; | |||
| 106 | static char buf[PATH_MAX4096]; | |||
| 107 | ||||
| 108 | return excl_fixed_path(ctx, EXCL_DROPIN_PATH"/usr/local/etc" "/nvme" "/" "exclusions.conf.d", EXCL_DROPIN_NAME"exclusions.conf.d", | |||
| 109 | &cache, buf, sizeof(buf)); | |||
| 110 | } | |||
| 111 | ||||
| 112 | /* Path of the main hand-edited list (<base>/exclusions.conf). */ | |||
| 113 | static const char *excl_main_path(struct libnvme_global_ctx *ctx) | |||
| 114 | { | |||
| 115 | static const char *cache; | |||
| 116 | static char buf[PATH_MAX4096]; | |||
| 117 | ||||
| 118 | return excl_fixed_path(ctx, EXCL_MAIN_PATH"/usr/local/etc" "/nvme" "/" "exclusions.conf", EXCL_MAIN_NAME"exclusions.conf", | |||
| 119 | &cache, buf, sizeof(buf)); | |||
| 120 | } | |||
| 121 | ||||
| 122 | /* | |||
| 123 | * Directory that contains a list's file -- where its atomic-write temp file is | |||
| 124 | * created and which is fsync'd to make a rename durable. The default list | |||
| 125 | * (@name == NULL) sits directly under the base dir; a named list is a drop-in. | |||
| 126 | */ | |||
| 127 | static const char *excl_dir(struct libnvme_global_ctx *ctx, const char *name) | |||
| 128 | { | |||
| 129 | return name ? excl_dropin_dir(ctx) : excl_base(ctx); | |||
| 130 | } | |||
| 131 | ||||
| 132 | /* | |||
| 133 | * Path of a list file. @name == NULL is the main hand-edited list; a non-NULL | |||
| 134 | * name is a managed drop-in (<base>/exclusions.conf.d/<name>.conf). Validating | |||
| 135 | * the name here (with the shared shr_valid_name()) guards every public entry | |||
| 136 | * point uniformly against unsafe characters and path traversal. The named path | |||
| 137 | * varies with @name so it is built into @buf; the main path is a fixed literal. | |||
| 138 | * Returns NULL on an invalid name or truncation. | |||
| 139 | */ | |||
| 140 | static const char *excl_path(struct libnvme_global_ctx *ctx, | |||
| 141 | const char *name, char *buf, size_t len) | |||
| 142 | { | |||
| 143 | int n; | |||
| 144 | ||||
| 145 | if (!name) | |||
| 146 | return excl_main_path(ctx); | |||
| 147 | ||||
| 148 | if (!shr_valid_name(name)) | |||
| 149 | return NULL((void*)0); | |||
| 150 | ||||
| 151 | n = snprintf(buf, len, "%s/%s.conf", excl_dropin_dir(ctx), name); | |||
| 152 | return (n > 0 && (size_t)n < len) ? buf : NULL((void*)0); | |||
| 153 | } | |||
| 154 | ||||
| 155 | static int ensure_excl_dir(struct libnvme_global_ctx *ctx, const char *name) | |||
| 156 | { | |||
| 157 | const char *dir = excl_dir(ctx, name); | |||
| 158 | ||||
| 159 | if (!dir) | |||
| 160 | return -ENAMETOOLONG36; | |||
| 161 | return shr_mkdir_p(dir, 0755); | |||
| 162 | } | |||
| 163 | ||||
| 164 | /* Build the atomic-write temp template "<dir>/.excl.tmp.XXXXXX" in @tmp. */ | |||
| 165 | static int excl_tmp(const char *dir, char *tmp, size_t len) | |||
| 166 | { | |||
| 167 | int n = snprintf(tmp, len, "%s/.excl.tmp.XXXXXX", dir); | |||
| 168 | ||||
| 169 | return (n > 0 && (size_t)n < len) ? 0 : -ENAMETOOLONG36; | |||
| 170 | } | |||
| 171 | ||||
| 172 | static bool_Bool addr_equal(const char *entry_val, const char *caller_val, | |||
| 173 | const char *transport) | |||
| 174 | { | |||
| 175 | if (shr_streq0(transport, "fc")) | |||
| 176 | return shr_streqcase0(entry_val, caller_val); | |||
| 177 | ||||
| 178 | return libnvme_ipaddrs_eq(entry_val, caller_val) || | |||
| 179 | shr_streq0(entry_val, caller_val); | |||
| 180 | } | |||
| 181 | ||||
| 182 | /* | |||
| 183 | * Minimal match: only the fields the entry (@e) actually sets are checked | |||
| 184 | * against the target @tid. traddr/host_traddr use addr_equal() for | |||
| 185 | * transport-aware address comparison; the rest compare verbatim. | |||
| 186 | */ | |||
| 187 | static bool_Bool tid_subset_match(const struct libnvmf_tid *e, | |||
| 188 | const struct libnvmf_tid *tid) | |||
| 189 | { | |||
| 190 | if (e->transport && !shr_streq0(e->transport, tid->transport)) | |||
| 191 | return false0; | |||
| 192 | if (e->traddr && | |||
| 193 | !(tid->traddr && | |||
| 194 | addr_equal(e->traddr, tid->traddr, tid->transport))) | |||
| 195 | return false0; | |||
| 196 | if (e->trsvcid && !shr_streq0(e->trsvcid, tid->trsvcid)) | |||
| 197 | return false0; | |||
| 198 | if (e->subsysnqn && !shr_streq0(e->subsysnqn, tid->subsysnqn)) | |||
| 199 | return false0; | |||
| 200 | if (e->host_traddr && | |||
| 201 | !(tid->host_traddr && | |||
| 202 | addr_equal(e->host_traddr, tid->host_traddr, tid->transport))) | |||
| 203 | return false0; | |||
| 204 | if (e->host_iface && !shr_streq0(e->host_iface, tid->host_iface)) | |||
| 205 | return false0; | |||
| 206 | if (e->hostnqn && !shr_streq0(e->hostnqn, tid->hostnqn)) | |||
| 207 | return false0; | |||
| 208 | if (e->hostid && !shr_streq0(e->hostid, tid->hostid)) | |||
| 209 | return false0; | |||
| 210 | return true1; | |||
| 211 | } | |||
| 212 | ||||
| 213 | /* | |||
| 214 | * Check one entry against a transport ID (minimal match): the entry is parsed | |||
| 215 | * into a partial TID, and only the fields it sets are compared against @tid. A | |||
| 216 | * malformed entry (unknown key, bare token, or empty value) or one that sets no | |||
| 217 | * fields matches nothing -- we never guess. Parsed silently (ctx == NULL): | |||
| 218 | * matching runs on every connect, so a bad hand-edited entry must stay quiet. | |||
| 219 | */ | |||
| 220 | static bool_Bool entry_matches(const char *entry, const struct libnvmf_tid *tid) | |||
| 221 | { | |||
| 222 | struct libnvmf_tid *e; | |||
| 223 | bool_Bool matches; | |||
| 224 | ||||
| 225 | if (libnvmf_tid_parse_strict(NULL((void*)0), entry, &e)) | |||
| 226 | return false0; | |||
| 227 | if (libnvmf_tid_is_empty(e)) { | |||
| 228 | libnvmf_tid_free(e); | |||
| 229 | return false0; | |||
| 230 | } | |||
| 231 | ||||
| 232 | matches = tid_subset_match(e, tid); | |||
| 233 | libnvmf_tid_free(e); | |||
| 234 | return matches; | |||
| 235 | } | |||
| 236 | ||||
| 237 | /* | |||
| 238 | * Validate an entry before writing it: it must parse cleanly (every key known, | |||
| 239 | * no malformed token) and set at least one field. @ctx is used only to log | |||
| 240 | * what was wrong; it may be NULL to validate silently. | |||
| 241 | */ | |||
| 242 | static bool_Bool entry_valid(struct libnvme_global_ctx *ctx, const char *entry) | |||
| 243 | { | |||
| 244 | struct libnvmf_tid *e; | |||
| 245 | bool_Bool valid; | |||
| 246 | ||||
| 247 | valid = !libnvmf_tid_parse_strict(ctx, entry, &e) && | |||
| 248 | !libnvmf_tid_is_empty(e); | |||
| 249 | ||||
| 250 | libnvmf_tid_free(e); | |||
| 251 | return valid; | |||
| 252 | } | |||
| 253 | ||||
| 254 | __shr_public__attribute__((visibility("default"))) bool_Bool libnvmf_exclusion_entry_valid(struct libnvme_global_ctx *ctx, | |||
| 255 | const char *entry) | |||
| 256 | { | |||
| 257 | if (!ctx) | |||
| 258 | return false0; | |||
| 259 | return entry_valid(ctx, entry); | |||
| 260 | } | |||
| 261 | ||||
| 262 | /* | |||
| 263 | * Scan one .conf file. For match scan: call entry_matches() on each entry, | |||
| 264 | * returning true on first match. For iteration: call the callback on each entry. | |||
| 265 | */ | |||
| 266 | enum excl_line_type { | |||
| 267 | EXCL_LINE_IGNORE, /* blank, comment, foreign key or foreign section */ | |||
| 268 | EXCL_LINE_SECTION, /* well-formed section header; *in_excl updated */ | |||
| 269 | EXCL_LINE_ENTRY, /* "exclusion =" inside [exclusions]; *val set */ | |||
| 270 | EXCL_LINE_STRAY, /* "exclusion =" outside [exclusions] */ | |||
| 271 | EXCL_LINE_JUNK, /* malformed section header */ | |||
| 272 | }; | |||
| 273 | ||||
| 274 | /* | |||
| 275 | * Classify one line of an exclusion list; the single scan step shared by the | |||
| 276 | * read, validate, add and remove paths so they can never drift apart. @s is | |||
| 277 | * a trimmed, mutable scratch copy of the line. @in_excl carries the "inside | |||
| 278 | * [exclusions]?" state across calls (start it at false); a malformed section | |||
| 279 | * header clears it, so the entries that follow are ignored rather than | |||
| 280 | * misattributed -- the fail-safe direction. | |||
| 281 | */ | |||
| 282 | static enum excl_line_type classify_line(char *s, bool_Bool *in_excl, char **val) | |||
| 283 | { | |||
| 284 | char *eq, *key; | |||
| 285 | ||||
| 286 | if (!*s || *s == '#') | |||
| 287 | return EXCL_LINE_IGNORE; | |||
| 288 | ||||
| 289 | if (*s == '[') { | |||
| 290 | char *end = strchr(s, ']')_Generic (0 ? (s) : (void *) 1, const void *: (const char *) ( strchr (s, ']')), default: strchr (s, ']')); | |||
| 291 | ||||
| 292 | if (!end) { | |||
| 293 | *in_excl = false0; | |||
| 294 | return EXCL_LINE_JUNK; | |||
| 295 | } | |||
| 296 | *end = '\0'; | |||
| 297 | *in_excl = !strcmp(shr_trim(s + 1), EXCL_SECTION"exclusions"); | |||
| 298 | return EXCL_LINE_SECTION; | |||
| 299 | } | |||
| 300 | ||||
| 301 | eq = strchr(s, '=')_Generic (0 ? (s) : (void *) 1, const void *: (const char *) ( strchr (s, '=')), default: strchr (s, '=')); | |||
| 302 | if (!eq) | |||
| 303 | return EXCL_LINE_IGNORE; | |||
| 304 | *eq = '\0'; | |||
| 305 | key = shr_trim(s); | |||
| 306 | if (strcmp(key, EXCL_LINE_KEY"exclusion")) | |||
| 307 | return EXCL_LINE_IGNORE; | |||
| 308 | if (!*in_excl) | |||
| 309 | return EXCL_LINE_STRAY; | |||
| 310 | ||||
| 311 | *val = shr_trim(eq + 1); | |||
| 312 | return EXCL_LINE_ENTRY; | |||
| 313 | } | |||
| 314 | ||||
| 315 | typedef bool_Bool (*scan_fn)(const char *entry, void *ctx); | |||
| 316 | ||||
| 317 | static bool_Bool scan_conf_file(const char *path, scan_fn fn, void *ctx) | |||
| 318 | { | |||
| 319 | FILE *f; | |||
| 320 | char line[EXCL_LINE_MAX4096]; | |||
| 321 | bool_Bool in_excl = false0; | |||
| 322 | bool_Bool result = false0; | |||
| 323 | ||||
| 324 | f = fopen(path, "r"); | |||
| 325 | if (!f) | |||
| 326 | return false0; | |||
| 327 | ||||
| 328 | while (fgets(line, sizeof(line), f)) { | |||
| 329 | char *s = shr_trim(line); | |||
| 330 | char *val; | |||
| 331 | ||||
| 332 | if (classify_line(s, &in_excl, &val) != EXCL_LINE_ENTRY) | |||
| 333 | continue; | |||
| 334 | ||||
| 335 | if (fn(val, ctx)) { | |||
| 336 | result = true1; | |||
| 337 | break; | |||
| 338 | } | |||
| 339 | } | |||
| 340 | fclose(f); | |||
| 341 | return result; | |||
| 342 | } | |||
| 343 | ||||
| 344 | static bool_Bool match_entry(const char *entry, void *ctx) | |||
| 345 | { | |||
| 346 | return entry_matches(entry, ctx); | |||
| 347 | } | |||
| 348 | ||||
| 349 | __shr_public__attribute__((visibility("default"))) bool_Bool libnvmf_exclusion_match(struct libnvme_global_ctx *ctx, | |||
| 350 | const struct libnvmf_tid *tid) | |||
| 351 | { | |||
| 352 | const char *dir, *mainp; | |||
| 353 | DIR *d; | |||
| 354 | struct dirent *de; | |||
| 355 | bool_Bool found = false0; | |||
| 356 | ||||
| 357 | if (!ctx || !tid) | |||
| 358 | return false0; | |||
| 359 | ||||
| 360 | /* The hand-edited main list first, then each managed drop-in. */ | |||
| 361 | mainp = excl_main_path(ctx); | |||
| 362 | if (mainp && scan_conf_file(mainp, match_entry, (void *)tid)) | |||
| 363 | return true1; | |||
| 364 | ||||
| 365 | dir = excl_dropin_dir(ctx); | |||
| 366 | if (!dir) | |||
| 367 | return false0; | |||
| 368 | ||||
| 369 | d = opendir(dir); | |||
| 370 | if (!d) | |||
| 371 | return false0; /* fail-open: directory missing = nothing excluded */ | |||
| 372 | ||||
| 373 | while ((de = readdir(d)) && !found) { | |||
| 374 | char path[PATH_MAX4096]; | |||
| 375 | const char *dot = strrchr(de->d_name, '.')_Generic (0 ? (de->d_name) : (void *) 1, const void *: (const char *) (strrchr (de->d_name, '.')), default: strrchr (de ->d_name, '.')); | |||
| 376 | size_t nlen; | |||
| 377 | ||||
| 378 | if (!dot || strcmp(dot, ".conf")) | |||
| 379 | continue; | |||
| 380 | ||||
| 381 | nlen = (size_t)(dot - de->d_name); | |||
| 382 | if (nlen == 0) | |||
| 383 | continue; | |||
| 384 | ||||
| 385 | if (snprintf(path, sizeof(path), "%s/%s", dir, | |||
| 386 | de->d_name) >= (int)sizeof(path)) | |||
| 387 | continue; | |||
| 388 | found = scan_conf_file(path, match_entry, (void *)tid); | |||
| 389 | } | |||
| 390 | closedir(d); | |||
| 391 | return found; | |||
| 392 | } | |||
| 393 | ||||
| 394 | struct iter_ctx { | |||
| 395 | void (*callback)(const char *entry, void *user_data); | |||
| 396 | void *user_data; | |||
| 397 | }; | |||
| 398 | ||||
| 399 | static bool_Bool iter_entry(const char *entry, void *ctx) | |||
| 400 | { | |||
| 401 | struct iter_ctx *ic = ctx; | |||
| 402 | ||||
| 403 | ic->callback(entry, ic->user_data); | |||
| 404 | return false0; /* never stop early */ | |||
| 405 | } | |||
| 406 | ||||
| 407 | __shr_public__attribute__((visibility("default"))) int libnvmf_exclusion_list_for_each( | |||
| 408 | struct libnvme_global_ctx *ctx, | |||
| 409 | void (*callback)(const char *name, void *user_data), | |||
| 410 | void *user_data) | |||
| 411 | { | |||
| 412 | const char *dir; | |||
| 413 | DIR *d; | |||
| 414 | struct dirent *de; | |||
| 415 | ||||
| 416 | if (!ctx) | |||
| 417 | return -EINVAL22; | |||
| 418 | ||||
| 419 | dir = excl_dropin_dir(ctx); | |||
| 420 | if (!dir) | |||
| 421 | return -ENAMETOOLONG36; | |||
| 422 | ||||
| 423 | d = opendir(dir); | |||
| 424 | if (!d) { | |||
| 425 | if (errno(*__errno_location ()) == ENOENT2) | |||
| 426 | return 0; | |||
| 427 | return -errno(*__errno_location ()); | |||
| 428 | } | |||
| 429 | ||||
| 430 | while ((de = readdir(d))) { | |||
| 431 | char name_buf[NAME_MAX255]; | |||
| 432 | const char *dot; | |||
| 433 | size_t nlen; | |||
| 434 | ||||
| 435 | dot = strrchr(de->d_name, '.')_Generic (0 ? (de->d_name) : (void *) 1, const void *: (const char *) (strrchr (de->d_name, '.')), default: strrchr (de ->d_name, '.')); | |||
| 436 | if (!dot || strcmp(dot, ".conf")) | |||
| 437 | continue; | |||
| 438 | ||||
| 439 | nlen = (size_t)(dot - de->d_name); | |||
| 440 | if (nlen == 0 || nlen >= sizeof(name_buf)) | |||
| 441 | continue; | |||
| 442 | ||||
| 443 | memcpy(name_buf, de->d_name, nlen); | |||
| 444 | name_buf[nlen] = '\0'; | |||
| 445 | callback(name_buf, user_data); | |||
| 446 | } | |||
| 447 | closedir(d); | |||
| 448 | return 0; | |||
| 449 | } | |||
| 450 | ||||
| 451 | __shr_public__attribute__((visibility("default"))) int libnvmf_exclusion_entry_for_each( | |||
| 452 | struct libnvme_global_ctx *ctx, | |||
| 453 | const char *name, | |||
| 454 | void (*callback)(const char *entry, void *user_data), | |||
| 455 | void *user_data) | |||
| 456 | { | |||
| 457 | const char *path; | |||
| 458 | char pathbuf[PATH_MAX4096]; | |||
| 459 | struct iter_ctx ic = { .callback = callback, .user_data = user_data }; | |||
| 460 | ||||
| 461 | if (!ctx) | |||
| 462 | return -EINVAL22; | |||
| 463 | ||||
| 464 | path = excl_path(ctx, name, pathbuf, sizeof(pathbuf)); | |||
| 465 | if (!path) | |||
| 466 | return -EINVAL22; | |||
| 467 | ||||
| 468 | if (access(path, F_OK0) < 0) | |||
| 469 | return -ENOENT2; | |||
| 470 | ||||
| 471 | scan_conf_file(path, iter_entry, &ic); | |||
| 472 | return 0; | |||
| 473 | } | |||
| 474 | ||||
| 475 | __shr_public__attribute__((visibility("default"))) int libnvmf_exclusion_create(struct libnvme_global_ctx *ctx, | |||
| 476 | const char *name) | |||
| 477 | { | |||
| 478 | const char *path; | |||
| 479 | char pathbuf[PATH_MAX4096]; | |||
| 480 | int fd, ret; | |||
| 481 | ||||
| 482 | if (!ctx) | |||
| 483 | return -EINVAL22; | |||
| 484 | ||||
| 485 | ret = ensure_excl_dir(ctx, name); | |||
| 486 | if (ret) | |||
| 487 | return ret; | |||
| 488 | ||||
| 489 | path = excl_path(ctx, name, pathbuf, sizeof(pathbuf)); | |||
| 490 | if (!path) | |||
| 491 | return -EINVAL22; | |||
| 492 | ||||
| 493 | fd = open(path, O_CREAT0100 | O_EXCL0200 | O_WRONLY01, 0644); | |||
| 494 | if (fd < 0) | |||
| 495 | return -errno(*__errno_location ()); | |||
| 496 | ||||
| 497 | /* | |||
| 498 | * Set the mode explicitly: O_CREAT honors the caller's umask, so a tight | |||
| 499 | * root umask would otherwise yield a non-world-readable list. Exclusion | |||
| 500 | * lists follow /etc/nvme policy -- readable by all, writable by root. | |||
| 501 | */ | |||
| 502 | if (fchmod(fd, 0644) < 0) { | |||
| 503 | ret = -errno(*__errno_location ()); | |||
| 504 | close(fd); | |||
| 505 | unlink(path); | |||
| 506 | return ret; | |||
| 507 | } | |||
| 508 | ||||
| 509 | /* Write the standard header comment. */ | |||
| 510 | dprintf(fd, EXCL_HEADER_FMT"# NVMe-oF exclusion list: %s\n" "# Format: exclusion = key=val;key=val\n" "# Keys: transport, traddr, trsvcid, nqn, host-traddr, host-iface, hostnqn, hostid\n" "\n" "[" "exclusions" "]\n", name ? name : "default"); | |||
| 511 | close(fd); | |||
| 512 | return 0; | |||
| 513 | } | |||
| 514 | ||||
| 515 | __shr_public__attribute__((visibility("default"))) int libnvmf_exclusion_delete(struct libnvme_global_ctx *ctx, | |||
| 516 | const char *name) | |||
| 517 | { | |||
| 518 | const char *path; | |||
| 519 | char pathbuf[PATH_MAX4096]; | |||
| 520 | ||||
| 521 | if (!ctx) | |||
| 522 | return -EINVAL22; | |||
| 523 | ||||
| 524 | path = excl_path(ctx, name, pathbuf, sizeof(pathbuf)); | |||
| 525 | if (!path) | |||
| 526 | return -EINVAL22; | |||
| 527 | ||||
| 528 | if (unlink(path) < 0) | |||
| 529 | return -errno(*__errno_location ()); | |||
| 530 | return 0; | |||
| 531 | } | |||
| 532 | ||||
| 533 | __shr_public__attribute__((visibility("default"))) int libnvmf_exclusion_add(struct libnvme_global_ctx *ctx, | |||
| 534 | const char *name, const char *entry) | |||
| 535 | { | |||
| 536 | char pathbuf[PATH_MAX4096], tmp[PATH_MAX4096], line[EXCL_LINE_MAX4096]; | |||
| 537 | const char *path, *dir; | |||
| 538 | FILE *fin, *fout; | |||
| 539 | int fd, ret = 0; | |||
| 540 | ||||
| 541 | if (!ctx) | |||
| 542 | return -EINVAL22; | |||
| 543 | if (!entry_valid(ctx, entry)) | |||
| 544 | return -EINVAL22; | |||
| 545 | ||||
| 546 | ret = ensure_excl_dir(ctx, name); | |||
| 547 | if (ret) | |||
| 548 | return ret; | |||
| 549 | ||||
| 550 | path = excl_path(ctx, name, pathbuf, sizeof(pathbuf)); | |||
| 551 | if (!path) | |||
| 552 | return -EINVAL22; | |||
| 553 | dir = excl_dir(ctx, name); | |||
| 554 | if (!dir || excl_tmp(dir, tmp, sizeof(tmp))) | |||
| 555 | return -ENAMETOOLONG36; | |||
| 556 | ||||
| 557 | fd = shr_mkstemp(tmp); | |||
| 558 | if (fd < 0) | |||
| 559 | return fd; | |||
| 560 | ||||
| 561 | /* mkstemp creates 0600; widen to /etc/nvme policy (world-readable). */ | |||
| 562 | if (fchmod(fd, 0644) < 0) { | |||
| 563 | ret = -errno(*__errno_location ()); | |||
| 564 | close(fd); | |||
| 565 | unlink(tmp); | |||
| 566 | return ret; | |||
| 567 | } | |||
| 568 | ||||
| 569 | fout = fdopen(fd, "w"); | |||
| 570 | if (!fout) { | |||
| 571 | ret = -errno(*__errno_location ()); | |||
| 572 | close(fd); | |||
| 573 | unlink(tmp); | |||
| 574 | return ret; | |||
| 575 | } | |||
| 576 | ||||
| 577 | /* Copy existing content if the file exists. */ | |||
| 578 | fin = fopen(path, "r"); | |||
| 579 | if (fin) { | |||
| 580 | bool_Bool in_excl = false0, has_section = false0; | |||
| 581 | ||||
| 582 | while (fgets(line, sizeof(line), fin)) { | |||
| 583 | char parsebuf[EXCL_LINE_MAX4096]; | |||
| 584 | char *val; | |||
| 585 | ||||
| 586 | fputs(line, fout); | |||
| 587 | ||||
| 588 | /* Classify a scratch copy; "line" must stay intact. */ | |||
| 589 | strncpy(parsebuf, line, sizeof(parsebuf) - 1); | |||
| 590 | parsebuf[sizeof(parsebuf) - 1] = '\0'; | |||
| 591 | classify_line(shr_trim(parsebuf), &in_excl, &val); | |||
| 592 | has_section |= in_excl; | |||
| 593 | } | |||
| 594 | fclose(fin); | |||
| 595 | ||||
| 596 | /* | |||
| 597 | * A hand-made file may lack the [exclusions] header; appending | |||
| 598 | * the entry bare would leave it outside the section, where the | |||
| 599 | * readers ignore it. (Re-)open the section before appending -- | |||
| 600 | * a repeated header is legal INI and merely re-enters it. | |||
| 601 | */ | |||
| 602 | if (!has_section || !in_excl) | |||
| 603 | fprintf(fout, "\n[%s]\n", EXCL_SECTION"exclusions"); | |||
| 604 | } else { | |||
| 605 | fprintf(fout, EXCL_HEADER_FMT"# NVMe-oF exclusion list: %s\n" "# Format: exclusion = key=val;key=val\n" "# Keys: transport, traddr, trsvcid, nqn, host-traddr, host-iface, hostnqn, hostid\n" "\n" "[" "exclusions" "]\n", name ? name : "default"); | |||
| 606 | } | |||
| 607 | ||||
| 608 | fprintf(fout, "%s = %s\n", EXCL_LINE_KEY"exclusion", entry); | |||
| 609 | ||||
| 610 | if (fflush(fout) != 0 || fsync(fileno(fout)) != 0) { | |||
| 611 | ret = -errno(*__errno_location ()); | |||
| 612 | fclose(fout); | |||
| 613 | unlink(tmp); | |||
| 614 | return ret; | |||
| 615 | } | |||
| 616 | if (fclose(fout) != 0) { | |||
| 617 | ret = -errno(*__errno_location ()); | |||
| 618 | unlink(tmp); | |||
| 619 | return ret; | |||
| 620 | } | |||
| 621 | ||||
| 622 | if (rename(tmp, path) < 0) { | |||
| 623 | ret = -errno(*__errno_location ()); | |||
| 624 | unlink(tmp); | |||
| 625 | return ret; | |||
| 626 | } | |||
| 627 | shr_fsync_dir(dir); /* make the rename durable */ | |||
| 628 | return ret; | |||
| 629 | } | |||
| 630 | ||||
| 631 | /* | |||
| 632 | * Build an exclusion entry string from a controller's transport parameters. | |||
| 633 | * It emits the transport-addressing tuple that identifies the path -- | |||
| 634 | * transport, traddr and subsysnqn unconditionally, trsvcid and host-iface | |||
| 635 | * when set -- and deliberately omits the host identity (hostnqn/hostid), so an | |||
| 636 | * exclusion built from a controller applies regardless of which host persona | |||
| 637 | * is connecting. | |||
| 638 | */ | |||
| 639 | static int excl_entry_from_ctrl(libnvme_ctrl_t c, char *buf, size_t len) | |||
| 640 | { | |||
| 641 | int n = 0; | |||
| 642 | ||||
| 643 | if (!c->transport || !c->traddr || !c->subsysnqn) | |||
| 644 | return -EINVAL22; | |||
| 645 | ||||
| 646 | /* Emit only the fields that are present; never a bare "trsvcid=". */ | |||
| 647 | #define APPEND(fmt, ...) \ | |||
| 648 | do { \ | |||
| 649 | if (n >= 0 && (size_t)n < len) \ | |||
| 650 | n += snprintf(buf + n, len - n, fmt, ##__VA_ARGS__); \ | |||
| 651 | } while (0) | |||
| 652 | ||||
| 653 | APPEND("transport=%s;traddr=%s", c->transport, c->traddr); | |||
| 654 | if (c->trsvcid && *c->trsvcid) | |||
| 655 | APPEND(";trsvcid=%s", c->trsvcid); | |||
| 656 | APPEND(";nqn=%s", c->subsysnqn); | |||
| 657 | if (c->host_iface && *c->host_iface) | |||
| 658 | APPEND(";host-iface=%s", c->host_iface); | |||
| 659 | ||||
| 660 | #undef APPEND | |||
| 661 | ||||
| 662 | return (n > 0 && (size_t)n < len) ? 0 : -ENAMETOOLONG36; | |||
| 663 | } | |||
| 664 | ||||
| 665 | __shr_public__attribute__((visibility("default"))) int libnvmf_exclusion_add_ctrl(struct libnvme_global_ctx *ctx, | |||
| 666 | const char *name, | |||
| 667 | struct libnvme_ctrl *c) | |||
| 668 | { | |||
| 669 | char entry[EXCL_LINE_MAX4096]; | |||
| 670 | int ret; | |||
| 671 | ||||
| 672 | if (!ctx || !c) | |||
| 673 | return -EINVAL22; | |||
| 674 | ||||
| 675 | ret = excl_entry_from_ctrl(c, entry, sizeof(entry)); | |||
| 676 | if (ret) | |||
| 677 | return ret; | |||
| 678 | ||||
| 679 | return libnvmf_exclusion_add(ctx, name, entry); | |||
| 680 | } | |||
| 681 | ||||
| 682 | __shr_public__attribute__((visibility("default"))) int libnvmf_exclusion_add_subsysnqn( | |||
| 683 | struct libnvme_global_ctx *ctx, const char *name, | |||
| 684 | const char *subsysnqn) | |||
| 685 | { | |||
| 686 | char entry[EXCL_LINE_MAX4096]; | |||
| 687 | int n; | |||
| 688 | ||||
| 689 | if (!ctx || !subsysnqn || !*subsysnqn) | |||
| 690 | return -EINVAL22; | |||
| 691 | ||||
| 692 | n = snprintf(entry, sizeof(entry), "nqn=%s", subsysnqn); | |||
| 693 | if (n <= 0 || (size_t)n >= sizeof(entry)) | |||
| 694 | return -ENAMETOOLONG36; | |||
| 695 | ||||
| 696 | return libnvmf_exclusion_add(ctx, name, entry); | |||
| 697 | } | |||
| 698 | ||||
| 699 | __shr_public__attribute__((visibility("default"))) int libnvmf_exclusion_remove(struct libnvme_global_ctx *ctx, | |||
| 700 | const char *name, const char *entry) | |||
| 701 | { | |||
| 702 | char pathbuf[PATH_MAX4096], tmp[PATH_MAX4096], line[EXCL_LINE_MAX4096]; | |||
| 703 | const char *path, *dir; | |||
| 704 | bool_Bool in_excl = false0; | |||
| 705 | bool_Bool removed = false0; | |||
| 706 | FILE *fin, *fout; | |||
| 707 | int fd, ret = 0; | |||
| 708 | ||||
| 709 | if (!ctx) | |||
| 710 | return -EINVAL22; | |||
| 711 | ||||
| 712 | path = excl_path(ctx, name, pathbuf, sizeof(pathbuf)); | |||
| 713 | if (!path) | |||
| 714 | return -EINVAL22; | |||
| 715 | ||||
| 716 | fin = fopen(path, "r"); | |||
| 717 | if (!fin) | |||
| 718 | return -ENOENT2; | |||
| 719 | dir = excl_dir(ctx, name); | |||
| 720 | if (!dir || excl_tmp(dir, tmp, sizeof(tmp))) { | |||
| 721 | fclose(fin); | |||
| 722 | return -ENAMETOOLONG36; | |||
| 723 | } | |||
| 724 | ||||
| 725 | fd = shr_mkstemp(tmp); | |||
| 726 | if (fd < 0) { | |||
| 727 | ret = fd; | |||
| 728 | fclose(fin); | |||
| 729 | return ret; | |||
| 730 | } | |||
| 731 | ||||
| 732 | /* mkstemp creates 0600; widen to /etc/nvme policy (world-readable). */ | |||
| 733 | if (fchmod(fd, 0644) < 0) { | |||
| 734 | ret = -errno(*__errno_location ()); | |||
| 735 | close(fd); | |||
| 736 | unlink(tmp); | |||
| 737 | fclose(fin); | |||
| 738 | return ret; | |||
| 739 | } | |||
| 740 | ||||
| 741 | fout = fdopen(fd, "w"); | |||
| 742 | if (!fout) { | |||
| 743 | ret = -errno(*__errno_location ()); | |||
| 744 | close(fd); | |||
| 745 | unlink(tmp); | |||
| 746 | fclose(fin); | |||
| 747 | return ret; | |||
| 748 | } | |||
| 749 | ||||
| 750 | while (fgets(line, sizeof(line), fin)) { | |||
| 751 | char parsebuf[EXCL_LINE_MAX4096]; | |||
| 752 | char *val; | |||
| 753 | ||||
| 754 | /* Classify a scratch copy; shr_trim() mutates in place and | |||
| 755 | * would otherwise clobber the trailing newline in "line" before | |||
| 756 | * it gets passed through to fout. | |||
| 757 | */ | |||
| 758 | strncpy(parsebuf, line, sizeof(parsebuf) - 1); | |||
| 759 | parsebuf[sizeof(parsebuf) - 1] = '\0'; | |||
| 760 | ||||
| 761 | /* Everything except the entry being removed passes through. */ | |||
| 762 | if (classify_line(shr_trim(parsebuf), &in_excl, | |||
| 763 | &val) == EXCL_LINE_ENTRY && | |||
| 764 | !removed && !strcmp(val, entry)) | |||
| 765 | removed = true1; /* skip this line */ | |||
| 766 | else | |||
| 767 | fputs(line, fout); | |||
| 768 | } | |||
| 769 | ||||
| 770 | fclose(fin); | |||
| 771 | ||||
| 772 | if (fflush(fout) != 0 || fsync(fileno(fout)) != 0) { | |||
| 773 | ret = -errno(*__errno_location ()); | |||
| 774 | fclose(fout); | |||
| 775 | unlink(tmp); | |||
| 776 | return ret; | |||
| 777 | } | |||
| 778 | if (fclose(fout) != 0) { | |||
| 779 | ret = -errno(*__errno_location ()); | |||
| 780 | unlink(tmp); | |||
| 781 | return ret; | |||
| 782 | } | |||
| 783 | ||||
| 784 | if (!removed) { | |||
| 785 | unlink(tmp); | |||
| 786 | return -ENOENT2; | |||
| 787 | } | |||
| 788 | ||||
| 789 | if (rename(tmp, path) < 0) { | |||
| 790 | ret = -errno(*__errno_location ()); | |||
| 791 | unlink(tmp); | |||
| 792 | return ret; | |||
| 793 | } | |||
| 794 | shr_fsync_dir(dir); /* make the rename durable */ | |||
| 795 | return ret; | |||
| 796 | } | |||
| 797 | ||||
| 798 | /* | |||
| 799 | * FNV-1a 64-bit over a byte range. Used as an opaque optimistic-concurrency | |||
| 800 | * token: read() hands the caller the hash of the file it saw, write() refuses | |||
| 801 | * if the file no longer hashes to that value. Never returns 0 -- that value | |||
| 802 | * is reserved to mean "the list did not exist". | |||
| 803 | */ | |||
| 804 | static uint64_t content_hash(const char *buf, size_t len) | |||
| 805 | { | |||
| 806 | uint64_t h = shr_fnv1a_64(buf, len); | |||
| 807 | ||||
| 808 | return h ? h : 1; | |||
| 809 | } | |||
| 810 | ||||
| 811 | /* | |||
| 812 | * Read the whole file at @path into a newly allocated, NUL-terminated buffer. | |||
| 813 | * On success sets *out (caller frees) and *len (excluding the NUL), returns 0. | |||
| 814 | * Returns -ENOENT if the file does not exist, or a negative errno otherwise. | |||
| 815 | */ | |||
| 816 | static int slurp(const char *path, char **out, size_t *len) | |||
| 817 | { | |||
| 818 | struct stat st; | |||
| 819 | char *buf; | |||
| 820 | size_t off = 0; | |||
| 821 | int fd, ret = 0; | |||
| 822 | ||||
| 823 | fd = open(path, O_RDONLY00 | O_CLOEXEC02000000); | |||
| 824 | if (fd < 0) | |||
| 825 | return -errno(*__errno_location ()); | |||
| 826 | if (fstat(fd, &st) < 0) { | |||
| 827 | ret = -errno(*__errno_location ()); | |||
| 828 | goto out; | |||
| 829 | } | |||
| 830 | if (st.st_size > EXCL_FILE_MAX(1 * 1024 * 1024)) { | |||
| 831 | ret = -EFBIG27; | |||
| 832 | goto out; | |||
| 833 | } | |||
| 834 | ||||
| 835 | buf = malloc(st.st_size + 1); | |||
| 836 | if (!buf) { | |||
| 837 | ret = -ENOMEM12; | |||
| 838 | goto out; | |||
| 839 | } | |||
| 840 | ||||
| 841 | while (off < (size_t)st.st_size) { | |||
| 842 | ssize_t n = read(fd, buf + off, st.st_size - off); | |||
| 843 | ||||
| 844 | if (n < 0) { | |||
| 845 | if (errno(*__errno_location ()) == EINTR4) | |||
| 846 | continue; | |||
| 847 | free(buf); | |||
| 848 | ret = -errno(*__errno_location ()); | |||
| 849 | goto out; | |||
| 850 | } | |||
| 851 | if (n == 0) | |||
| 852 | break; | |||
| 853 | off += n; | |||
| 854 | } | |||
| 855 | buf[off] = '\0'; | |||
| 856 | *out = buf; | |||
| 857 | *len = off; | |||
| 858 | out: | |||
| 859 | close(fd); | |||
| 860 | return ret; | |||
| 861 | } | |||
| 862 | ||||
| 863 | /* Hash the current on-disk list. Sets *out to 0 when the list is absent. */ | |||
| 864 | static int hash_file(const char *path, uint64_t *out) | |||
| 865 | { | |||
| 866 | __cleanup_free__attribute__((cleanup(shr_freep))) char *buf = NULL((void*)0); | |||
| 867 | size_t len; | |||
| 868 | int ret; | |||
| 869 | ||||
| 870 | ret = slurp(path, &buf, &len); | |||
| 871 | if (ret == -ENOENT2) { | |||
| 872 | *out = 0; | |||
| 873 | return 0; | |||
| 874 | } | |||
| 875 | if (ret) | |||
| 876 | return ret; | |||
| 877 | *out = content_hash(buf, len); | |||
| 878 | return 0; | |||
| 879 | } | |||
| 880 | ||||
| 881 | /* | |||
| 882 | * Validate every "exclusion = ..." line in @text. Comments, blank lines and | |||
| 883 | * non-exclusion keys are ignored. Returns 0 if all entries are valid, -EINVAL | |||
| 884 | * otherwise. The public write path validates here too -- it cannot trust the | |||
| 885 | * caller to have pre-checked the buffer. Writing is stricter than reading: | |||
| 886 | * a malformed section header or an entry outside [exclusions] would be | |||
| 887 | * silently skipped by the readers (disarming the entry), so reject the buffer | |||
| 888 | * loudly here instead of letting an editor persist it. | |||
| 889 | */ | |||
| 890 | static int validate_conf_buf(struct libnvme_global_ctx *ctx, const char *text) | |||
| 891 | { | |||
| 892 | __cleanup_free__attribute__((cleanup(shr_freep))) char *copy = strdup(text); | |||
| 893 | char *save = NULL((void*)0), *line; | |||
| 894 | bool_Bool in_excl = false0; | |||
| 895 | int ret = 0; | |||
| 896 | ||||
| 897 | if (!copy) | |||
| 898 | return -ENOMEM12; | |||
| 899 | ||||
| 900 | for (line = strtok_r(copy, "\n", &save); line; | |||
| ||||
| 901 | line = strtok_r(NULL((void*)0), "\n", &save)) { | |||
| 902 | char *s = shr_trim(line), *val; | |||
| 903 | ||||
| 904 | switch (classify_line(s, &in_excl, &val)) { | |||
| 905 | case EXCL_LINE_ENTRY: | |||
| 906 | if (!entry_valid(ctx, val)) | |||
| 907 | ret = -EINVAL22; | |||
| 908 | break; | |||
| 909 | case EXCL_LINE_STRAY: | |||
| 910 | case EXCL_LINE_JUNK: | |||
| 911 | ret = -EINVAL22; | |||
| 912 | break; | |||
| 913 | default: | |||
| 914 | break; | |||
| 915 | } | |||
| 916 | if (ret) | |||
| 917 | break; | |||
| 918 | } | |||
| 919 | return ret; | |||
| 920 | } | |||
| 921 | ||||
| 922 | __shr_public__attribute__((visibility("default"))) int libnvmf_exclusion_read(struct libnvme_global_ctx *ctx, | |||
| 923 | const char *name, char **text, | |||
| 924 | uint64_t *version) | |||
| 925 | { | |||
| 926 | char pathbuf[PATH_MAX4096]; | |||
| 927 | const char *path; | |||
| 928 | size_t len = 0; | |||
| 929 | int ret; | |||
| 930 | ||||
| 931 | if (!ctx) | |||
| 932 | return -EINVAL22; | |||
| 933 | if (!text || !version) | |||
| 934 | return -EINVAL22; | |||
| 935 | *text = NULL((void*)0); | |||
| 936 | *version = 0; | |||
| 937 | ||||
| 938 | path = excl_path(ctx, name, pathbuf, sizeof(pathbuf)); | |||
| 939 | if (!path) | |||
| 940 | return -EINVAL22; | |||
| 941 | ||||
| 942 | ret = slurp(path, text, &len); | |||
| 943 | if (ret == -ENOENT2) { | |||
| 944 | /* A missing list reads as empty so an editor can create it. */ | |||
| 945 | *text = strdup(""); | |||
| 946 | return *text ? 0 : -ENOMEM12; | |||
| 947 | } | |||
| 948 | if (ret) | |||
| 949 | return ret; | |||
| 950 | ||||
| 951 | *version = content_hash(*text, len); | |||
| 952 | return 0; | |||
| 953 | } | |||
| 954 | ||||
| 955 | __shr_public__attribute__((visibility("default"))) int libnvmf_exclusion_write(struct libnvme_global_ctx *ctx, | |||
| 956 | const char *name, const char *text, | |||
| 957 | uint64_t version) | |||
| 958 | { | |||
| 959 | char pathbuf[PATH_MAX4096], tmp[PATH_MAX4096]; | |||
| 960 | const char *path, *dir; | |||
| 961 | uint64_t cur; | |||
| 962 | int dir_fd, fd, ret; | |||
| 963 | ||||
| 964 | if (!ctx) | |||
| ||||
| 965 | return -EINVAL22; | |||
| 966 | if (!text) | |||
| 967 | return -EINVAL22; | |||
| 968 | ||||
| 969 | ret = validate_conf_buf(ctx, text); | |||
| 970 | if (ret) | |||
| 971 | return ret; | |||
| 972 | ||||
| 973 | ret = ensure_excl_dir(ctx, name); | |||
| 974 | if (ret) | |||
| 975 | return ret; | |||
| 976 | ||||
| 977 | path = excl_path(ctx, name, pathbuf, sizeof(pathbuf)); | |||
| 978 | if (!path) | |||
| 979 | return -EINVAL22; | |||
| 980 | ||||
| 981 | /* | |||
| 982 | * Optimistic concurrency: serialize only the compare-and-swap window | |||
| 983 | * (recheck the on-disk version, then rename) under a directory lock. | |||
| 984 | * The editor ran unlocked, so two editors never block on each other -- | |||
| 985 | * the second to save sees a changed version and gets -ESTALE rather than | |||
| 986 | * silently clobbering the first. | |||
| 987 | */ | |||
| 988 | dir = excl_dir(ctx, name); | |||
| 989 | if (!dir || excl_tmp(dir, tmp, sizeof(tmp))) | |||
| 990 | return -ENAMETOOLONG36; | |||
| 991 | dir_fd = open(dir, O_RDONLY00 | O_DIRECTORY0200000 | O_CLOEXEC02000000); | |||
| 992 | if (dir_fd < 0) | |||
| 993 | return -errno(*__errno_location ()); | |||
| 994 | if (flock(dir_fd, LOCK_EX2) < 0) { | |||
| 995 | ret = -errno(*__errno_location ()); | |||
| 996 | goto out; | |||
| 997 | } | |||
| 998 | ||||
| 999 | ret = hash_file(path, &cur); | |||
| 1000 | if (ret) | |||
| 1001 | goto out; | |||
| 1002 | if (cur != version) { | |||
| 1003 | ret = -ESTALE116; | |||
| 1004 | goto out; | |||
| 1005 | } | |||
| 1006 | ||||
| 1007 | fd = shr_mkstemp(tmp); | |||
| 1008 | if (fd < 0) { | |||
| 1009 | ret = fd; | |||
| 1010 | goto out; | |||
| 1011 | } | |||
| 1012 | ||||
| 1013 | /* mkstemp creates 0600; widen to /etc/nvme policy (world-readable). */ | |||
| 1014 | if (fchmod(fd, 0644) < 0) { | |||
| 1015 | ret = -errno(*__errno_location ()); | |||
| 1016 | goto err_tmp; | |||
| 1017 | } | |||
| 1018 | ret = shr_write_all(fd, text, strlen(text)); | |||
| 1019 | if (ret) | |||
| 1020 | goto err_tmp; | |||
| 1021 | if (fsync(fd) < 0) { | |||
| 1022 | ret = -errno(*__errno_location ()); | |||
| 1023 | goto err_tmp; | |||
| 1024 | } | |||
| 1025 | close(fd); | |||
| 1026 | ||||
| 1027 | if (rename(tmp, path) < 0) { | |||
| 1028 | ret = -errno(*__errno_location ()); | |||
| 1029 | unlink(tmp); | |||
| 1030 | goto out; | |||
| 1031 | } | |||
| 1032 | shr_fsync_dir(dir); /* make the rename durable */ | |||
| 1033 | ret = 0; | |||
| 1034 | goto out; | |||
| 1035 | ||||
| 1036 | err_tmp: | |||
| 1037 | close(fd); | |||
| 1038 | unlink(tmp); | |||
| 1039 | out: | |||
| 1040 | close(dir_fd); /* releases the flock */ | |||
| 1041 | return ret; | |||
| 1042 | } |