Bug Summary

File:.build-ci/../libnvme/src/nvme/exclusion.c
Warning:line 914, column 12
Potential leak of memory pointed to by 'copy'

Annotated Source Code

Press '?' to see keyboard shortcuts

clang -cc1 -cc1 -triple x86_64-redhat-linux-gnu -O3 -analyze -disable-free -clear-ast-before-backend -disable-llvm-verifier -discard-value-names -main-file-name exclusion.c -analyzer-checker=core -analyzer-checker=apiModeling -analyzer-checker=unix -analyzer-checker=deadcode -analyzer-checker=security.insecureAPI.UncheckedReturn -analyzer-checker=security.insecureAPI.getpw -analyzer-checker=security.insecureAPI.gets -analyzer-checker=security.insecureAPI.mktemp -analyzer-checker=security.insecureAPI.mkstemp -analyzer-checker=security.insecureAPI.vfork -analyzer-checker=nullability.NullPassedToNonnull -analyzer-checker=nullability.NullReturnedFromNonnull -analyzer-output plist -w -setup-static-analyzer -mrelocation-model pic -pic-level 2 -fhalf-no-semantic-interposition -mframe-pointer=none -fmath-errno -ffp-contract=on -fno-rounding-math -mconstructor-aliases -funwind-tables=2 -target-cpu x86-64 -tune-cpu generic -debugger-tuning=gdb -fdebug-compilation-dir=/__w/nvme-cli/nvme-cli/.build-ci -fcoverage-compilation-dir=/__w/nvme-cli/nvme-cli/.build-ci -resource-dir /usr/bin/../lib/clang/22 -include /__w/nvme-cli/nvme-cli/.build-ci/nvme-config.h -I libnvme/src/libnvme3.so.1.0.0.p -I libnvme/src -I ../libnvme/src -I ccan -I ../ccan -I . -I .. -D _FILE_OFFSET_BITS=64 -D _GNU_SOURCE -U NDEBUG -internal-isystem /usr/bin/../lib/clang/22/include -internal-isystem /usr/local/include -internal-isystem /usr/bin/../lib/gcc/x86_64-redhat-linux/16/../../../../x86_64-redhat-linux/include -internal-externc-isystem /include -internal-externc-isystem /usr/include -std=gnu11 -ferror-limit 19 -fvisibility=hidden -fgnuc-version=4.2.1 -fskip-odr-check-in-gmf -fcolor-diagnostics -vectorize-loops -vectorize-slp -analyzer-opt-analyze-headers -analyzer-output=html -faddrsig -fdwarf2-cfi-asm -o /__w/nvme-cli/nvme-cli/.build-ci/scan-results/2026-09-23-073103-589-1 -x c ../libnvme/src/nvme/exclusion.c
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 <shared/compiler-attributes-util.h>
33#include <shared/fs-util.h>
34#include <shared/hash-util.h>
35#include <shared/io-util.h>
36#include <shared/string-util.h>
37
38#include "cleanup.h"
39#include "exclusion.h"
40#include "lib.h"
41#include "nvme/generated/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 */
75static 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 */
85static 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). */
103static 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). */
113static 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 */
127static 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 */
140static 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
155static 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. */
165static 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
172static 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 */
187static 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. The parse reports what was wrong
218 * at DEBUG: matching runs on every connect, so a bad hand-edited entry must
219 * stay quiet by default. "nvme exclusion list" flags it for the admin.
220 */
221static bool_Bool entry_matches(struct libnvme_global_ctx *ctx, const char *entry,
222 const struct libnvmf_tid *tid)
223{
224 struct libnvmf_tid *e;
225 bool_Bool matches;
226
227 if (_libnvmf_tid_parse_strict_at_level(ctx, LIBNVME_LOG_DEBUG,
228 entry, &e))
229 return false0;
230 if (libnvmf_tid_is_empty(e)) {
231 libnvmf_tid_free(e);
232 return false0;
233 }
234
235 matches = tid_subset_match(e, tid);
236 libnvmf_tid_free(e);
237 return matches;
238}
239
240/*
241 * Validate an entry before writing it: it must parse cleanly (every key known,
242 * no malformed token) and set at least one field. @ctx must be non-NULL; it
243 * is used to log what was wrong.
244 */
245static bool_Bool entry_valid(struct libnvme_global_ctx *ctx, const char *entry)
246{
247 struct libnvmf_tid *e;
248 bool_Bool valid;
249
250 valid = !libnvmf_tid_parse_strict(ctx, entry, &e) &&
251 !libnvmf_tid_is_empty(e);
252
253 libnvmf_tid_free(e);
254 return valid;
255}
256
257__shr_public__attribute__((visibility("default"))) bool_Bool libnvmf_exclusion_entry_valid(struct libnvme_global_ctx *ctx,
258 const char *entry)
259{
260 if (!ctx)
261 return false0;
262 return entry_valid(ctx, entry);
263}
264
265/*
266 * Scan one .conf file. For match scan: call entry_matches() on each entry,
267 * returning true on first match. For iteration: call the callback on each entry.
268 */
269enum excl_line_type {
270 EXCL_LINE_IGNORE, /* blank, comment, foreign key or foreign section */
271 EXCL_LINE_SECTION, /* well-formed section header; *in_excl updated */
272 EXCL_LINE_ENTRY, /* "exclusion =" inside [exclusions]; *val set */
273 EXCL_LINE_STRAY, /* "exclusion =" outside [exclusions] */
274 EXCL_LINE_JUNK, /* malformed section header */
275};
276
277/*
278 * Classify one line of an exclusion list; the single scan step shared by the
279 * read, validate, add and remove paths so they can never drift apart. @s is
280 * a trimmed, mutable scratch copy of the line. @in_excl carries the "inside
281 * [exclusions]?" state across calls (start it at false); a malformed section
282 * header clears it, so the entries that follow are ignored rather than
283 * misattributed -- the fail-safe direction.
284 */
285static enum excl_line_type classify_line(char *s, bool_Bool *in_excl, char **val)
286{
287 char *eq, *key;
288
289 if (!*s || *s == '#')
290 return EXCL_LINE_IGNORE;
291
292 if (*s == '[') {
293 char *end = strchr(s, ']')_Generic (0 ? (s) : (void *) 1, const void *: (const char *) (
strchr (s, ']')), default: strchr (s, ']'))
;
294
295 if (!end) {
296 *in_excl = false0;
297 return EXCL_LINE_JUNK;
298 }
299 *end = '\0';
300 *in_excl = !strcmp(shr_trim(s + 1), EXCL_SECTION"exclusions");
301 return EXCL_LINE_SECTION;
302 }
303
304 eq = strchr(s, '=')_Generic (0 ? (s) : (void *) 1, const void *: (const char *) (
strchr (s, '=')), default: strchr (s, '='))
;
305 if (!eq)
306 return EXCL_LINE_IGNORE;
307 *eq = '\0';
308 key = shr_trim(s);
309 if (strcmp(key, EXCL_LINE_KEY"exclusion"))
310 return EXCL_LINE_IGNORE;
311 if (!*in_excl)
312 return EXCL_LINE_STRAY;
313
314 *val = shr_trim(eq + 1);
315 return EXCL_LINE_ENTRY;
316}
317
318typedef bool_Bool (*scan_fn)(const char *entry, void *user_data);
319
320static bool_Bool scan_conf_file(const char *path, scan_fn fn, void *user_data)
321{
322 FILE *f;
323 char line[EXCL_LINE_MAX4096];
324 bool_Bool in_excl = false0;
325 bool_Bool result = false0;
326
327 f = fopen(path, "r");
328 if (!f)
329 return false0;
330
331 while (fgets(line, sizeof(line), f)) {
332 char *s = shr_trim(line);
333 char *val;
334
335 if (classify_line(s, &in_excl, &val) != EXCL_LINE_ENTRY)
336 continue;
337
338 if (fn(val, user_data)) {
339 result = true1;
340 break;
341 }
342 }
343 fclose(f);
344 return result;
345}
346
347struct match_ctx {
348 struct libnvme_global_ctx *ctx;
349 const struct libnvmf_tid *tid;
350};
351
352static bool_Bool match_entry(const char *entry, void *user_data)
353{
354 const struct match_ctx *mc = user_data;
355
356 return entry_matches(mc->ctx, entry, mc->tid);
357}
358
359__shr_public__attribute__((visibility("default"))) bool_Bool libnvmf_exclusion_match(struct libnvme_global_ctx *ctx,
360 const struct libnvmf_tid *tid)
361{
362 const char *dir, *mainp;
363 struct match_ctx mc;
364 DIR *d;
365 struct dirent *de;
366 bool_Bool found = false0;
367
368 if (!ctx || !tid)
369 return false0;
370
371 mc.ctx = ctx;
372 mc.tid = tid;
373
374 /* The hand-edited main list first, then each managed drop-in. */
375 mainp = excl_main_path(ctx);
376 if (mainp && scan_conf_file(mainp, match_entry, &mc))
377 return true1;
378
379 dir = excl_dropin_dir(ctx);
380 if (!dir)
381 return false0;
382
383 d = opendir(dir);
384 if (!d)
385 return false0; /* fail-open: directory missing = nothing excluded */
386
387 while ((de = readdir(d)) && !found) {
388 char path[PATH_MAX4096];
389 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, '.'))
;
390 size_t nlen;
391
392 if (!dot || strcmp(dot, ".conf"))
393 continue;
394
395 nlen = (size_t)(dot - de->d_name);
396 if (nlen == 0)
397 continue;
398
399 if (snprintf(path, sizeof(path), "%s/%s", dir,
400 de->d_name) >= (int)sizeof(path))
401 continue;
402 found = scan_conf_file(path, match_entry, &mc);
403 }
404 closedir(d);
405 return found;
406}
407
408struct iter_ctx {
409 void (*callback)(const char *entry, void *user_data);
410 void *user_data;
411};
412
413static bool_Bool iter_entry(const char *entry, void *ctx)
414{
415 struct iter_ctx *ic = ctx;
416
417 ic->callback(entry, ic->user_data);
418 return false0; /* never stop early */
419}
420
421__shr_public__attribute__((visibility("default"))) int libnvmf_exclusion_list_for_each(
422 struct libnvme_global_ctx *ctx,
423 void (*callback)(const char *name, void *user_data),
424 void *user_data)
425{
426 const char *dir;
427 DIR *d;
428 struct dirent *de;
429
430 if (!ctx)
431 return -EINVAL22;
432
433 dir = excl_dropin_dir(ctx);
434 if (!dir)
435 return -ENAMETOOLONG36;
436
437 d = opendir(dir);
438 if (!d) {
439 if (errno(*__errno_location ()) == ENOENT2)
440 return 0;
441 return -errno(*__errno_location ());
442 }
443
444 while ((de = readdir(d))) {
445 char name_buf[NAME_MAX255];
446 const char *dot;
447 size_t nlen;
448
449 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, '.'))
;
450 if (!dot || strcmp(dot, ".conf"))
451 continue;
452
453 nlen = (size_t)(dot - de->d_name);
454 if (nlen == 0 || nlen >= sizeof(name_buf))
455 continue;
456
457 memcpy(name_buf, de->d_name, nlen);
458 name_buf[nlen] = '\0';
459 callback(name_buf, user_data);
460 }
461 closedir(d);
462 return 0;
463}
464
465__shr_public__attribute__((visibility("default"))) int libnvmf_exclusion_entry_for_each(
466 struct libnvme_global_ctx *ctx,
467 const char *name,
468 void (*callback)(const char *entry, void *user_data),
469 void *user_data)
470{
471 const char *path;
472 char pathbuf[PATH_MAX4096];
473 struct iter_ctx ic = { .callback = callback, .user_data = user_data };
474
475 if (!ctx)
476 return -EINVAL22;
477
478 path = excl_path(ctx, name, pathbuf, sizeof(pathbuf));
479 if (!path)
480 return -EINVAL22;
481
482 if (access(path, F_OK0) < 0)
483 return -ENOENT2;
484
485 scan_conf_file(path, iter_entry, &ic);
486 return 0;
487}
488
489__shr_public__attribute__((visibility("default"))) int libnvmf_exclusion_create(struct libnvme_global_ctx *ctx,
490 const char *name)
491{
492 const char *path;
493 char pathbuf[PATH_MAX4096];
494 int fd, ret;
495
496 if (!ctx)
497 return -EINVAL22;
498
499 ret = ensure_excl_dir(ctx, name);
500 if (ret)
501 return ret;
502
503 path = excl_path(ctx, name, pathbuf, sizeof(pathbuf));
504 if (!path)
505 return -EINVAL22;
506
507 fd = open(path, O_CREAT0100 | O_EXCL0200 | O_WRONLY01, 0644);
508 if (fd < 0)
509 return -errno(*__errno_location ());
510
511 /*
512 * Set the mode explicitly: O_CREAT honors the caller's umask, so a tight
513 * root umask would otherwise yield a non-world-readable list. Exclusion
514 * lists follow /etc/nvme policy -- readable by all, writable by root.
515 */
516 if (fchmod(fd, 0644) < 0) {
517 ret = -errno(*__errno_location ());
518 close(fd);
519 unlink(path);
520 return ret;
521 }
522
523 /* Write the standard header comment. */
524 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");
525 close(fd);
526 return 0;
527}
528
529__shr_public__attribute__((visibility("default"))) int libnvmf_exclusion_delete(struct libnvme_global_ctx *ctx,
530 const char *name)
531{
532 const char *path;
533 char pathbuf[PATH_MAX4096];
534
535 if (!ctx)
536 return -EINVAL22;
537
538 path = excl_path(ctx, name, pathbuf, sizeof(pathbuf));
539 if (!path)
540 return -EINVAL22;
541
542 if (unlink(path) < 0)
543 return -errno(*__errno_location ());
544 return 0;
545}
546
547__shr_public__attribute__((visibility("default"))) int libnvmf_exclusion_add(struct libnvme_global_ctx *ctx,
548 const char *name, const char *entry)
549{
550 char pathbuf[PATH_MAX4096], tmp[PATH_MAX4096], line[EXCL_LINE_MAX4096];
551 const char *path, *dir;
552 FILE *fin, *fout;
553 int fd, ret = 0;
554
555 if (!ctx)
556 return -EINVAL22;
557 if (!entry_valid(ctx, entry))
558 return -EINVAL22;
559
560 ret = ensure_excl_dir(ctx, name);
561 if (ret)
562 return ret;
563
564 path = excl_path(ctx, name, pathbuf, sizeof(pathbuf));
565 if (!path)
566 return -EINVAL22;
567 dir = excl_dir(ctx, name);
568 if (!dir || excl_tmp(dir, tmp, sizeof(tmp)))
569 return -ENAMETOOLONG36;
570
571 fd = shr_mkstemp(tmp);
572 if (fd < 0)
573 return fd;
574
575 /* mkstemp creates 0600; widen to /etc/nvme policy (world-readable). */
576 if (fchmod(fd, 0644) < 0) {
577 ret = -errno(*__errno_location ());
578 close(fd);
579 unlink(tmp);
580 return ret;
581 }
582
583 fout = fdopen(fd, "w");
584 if (!fout) {
585 ret = -errno(*__errno_location ());
586 close(fd);
587 unlink(tmp);
588 return ret;
589 }
590
591 /* Copy existing content if the file exists. */
592 fin = fopen(path, "r");
593 if (fin) {
594 bool_Bool in_excl = false0, has_section = false0;
595
596 while (fgets(line, sizeof(line), fin)) {
597 char parsebuf[EXCL_LINE_MAX4096];
598 char *val;
599
600 fputs(line, fout);
601
602 /* Classify a scratch copy; "line" must stay intact. */
603 strncpy(parsebuf, line, sizeof(parsebuf) - 1);
604 parsebuf[sizeof(parsebuf) - 1] = '\0';
605 classify_line(shr_trim(parsebuf), &in_excl, &val);
606 has_section |= in_excl;
607 }
608 fclose(fin);
609
610 /*
611 * A hand-made file may lack the [exclusions] header; appending
612 * the entry bare would leave it outside the section, where the
613 * readers ignore it. (Re-)open the section before appending --
614 * a repeated header is legal INI and merely re-enters it.
615 */
616 if (!has_section || !in_excl)
617 fprintf(fout, "\n[%s]\n", EXCL_SECTION"exclusions");
618 } else {
619 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");
620 }
621
622 fprintf(fout, "%s = %s\n", EXCL_LINE_KEY"exclusion", entry);
623
624 if (fflush(fout) != 0 || fsync(fileno(fout)) != 0) {
625 ret = -errno(*__errno_location ());
626 fclose(fout);
627 unlink(tmp);
628 return ret;
629 }
630 if (fclose(fout) != 0) {
631 ret = -errno(*__errno_location ());
632 unlink(tmp);
633 return ret;
634 }
635
636 if (rename(tmp, path) < 0) {
637 ret = -errno(*__errno_location ());
638 unlink(tmp);
639 return ret;
640 }
641 shr_fsync_dir(dir); /* make the rename durable */
642 return ret;
643}
644
645/*
646 * Build an exclusion entry string from a controller's transport parameters.
647 * It emits the transport-addressing tuple that identifies the path --
648 * transport, traddr and subsysnqn unconditionally, trsvcid and host-iface
649 * when set -- and deliberately omits the host identity (hostnqn/hostid), so an
650 * exclusion built from a controller applies regardless of which host persona
651 * is connecting.
652 */
653static int excl_entry_from_ctrl(struct libnvme_ctrl *c, char *buf, size_t len)
654{
655 int n = 0;
656
657 if (!c->transport || !c->traddr || !c->subsysnqn)
658 return -EINVAL22;
659
660 /* Emit only the fields that are present; never a bare "trsvcid=". */
661#define APPEND(fmt, ...) \
662 do { \
663 if (n >= 0 && (size_t)n < len) \
664 n += snprintf(buf + n, len - n, fmt, ##__VA_ARGS__); \
665 } while (0)
666
667 APPEND("transport=%s;traddr=%s", c->transport, c->traddr);
668 if (c->trsvcid && *c->trsvcid)
669 APPEND(";trsvcid=%s", c->trsvcid);
670 APPEND(";nqn=%s", c->subsysnqn);
671 if (c->host_iface && *c->host_iface)
672 APPEND(";host-iface=%s", c->host_iface);
673
674#undef APPEND
675
676 return (n > 0 && (size_t)n < len) ? 0 : -ENAMETOOLONG36;
677}
678
679__shr_public__attribute__((visibility("default"))) int libnvmf_exclusion_add_ctrl(struct libnvme_global_ctx *ctx,
680 const char *name,
681 struct libnvme_ctrl *c)
682{
683 char entry[EXCL_LINE_MAX4096];
684 int ret;
685
686 if (!ctx || !c)
687 return -EINVAL22;
688
689 ret = excl_entry_from_ctrl(c, entry, sizeof(entry));
690 if (ret)
691 return ret;
692
693 return libnvmf_exclusion_add(ctx, name, entry);
694}
695
696__shr_public__attribute__((visibility("default"))) int libnvmf_exclusion_add_subsysnqn(
697 struct libnvme_global_ctx *ctx, const char *name,
698 const char *subsysnqn)
699{
700 char entry[EXCL_LINE_MAX4096];
701 int n;
702
703 if (!ctx || !subsysnqn || !*subsysnqn)
704 return -EINVAL22;
705
706 n = snprintf(entry, sizeof(entry), "nqn=%s", subsysnqn);
707 if (n <= 0 || (size_t)n >= sizeof(entry))
708 return -ENAMETOOLONG36;
709
710 return libnvmf_exclusion_add(ctx, name, entry);
711}
712
713__shr_public__attribute__((visibility("default"))) int libnvmf_exclusion_remove(struct libnvme_global_ctx *ctx,
714 const char *name, const char *entry)
715{
716 char pathbuf[PATH_MAX4096], tmp[PATH_MAX4096], line[EXCL_LINE_MAX4096];
717 const char *path, *dir;
718 bool_Bool in_excl = false0;
719 bool_Bool removed = false0;
720 FILE *fin, *fout;
721 int fd, ret = 0;
722
723 if (!ctx)
724 return -EINVAL22;
725
726 path = excl_path(ctx, name, pathbuf, sizeof(pathbuf));
727 if (!path)
728 return -EINVAL22;
729
730 fin = fopen(path, "r");
731 if (!fin)
732 return -ENOENT2;
733 dir = excl_dir(ctx, name);
734 if (!dir || excl_tmp(dir, tmp, sizeof(tmp))) {
735 fclose(fin);
736 return -ENAMETOOLONG36;
737 }
738
739 fd = shr_mkstemp(tmp);
740 if (fd < 0) {
741 ret = fd;
742 fclose(fin);
743 return ret;
744 }
745
746 /* mkstemp creates 0600; widen to /etc/nvme policy (world-readable). */
747 if (fchmod(fd, 0644) < 0) {
748 ret = -errno(*__errno_location ());
749 close(fd);
750 unlink(tmp);
751 fclose(fin);
752 return ret;
753 }
754
755 fout = fdopen(fd, "w");
756 if (!fout) {
757 ret = -errno(*__errno_location ());
758 close(fd);
759 unlink(tmp);
760 fclose(fin);
761 return ret;
762 }
763
764 while (fgets(line, sizeof(line), fin)) {
765 char parsebuf[EXCL_LINE_MAX4096];
766 char *val;
767
768 /* Classify a scratch copy; shr_trim() mutates in place and
769 * would otherwise clobber the trailing newline in "line" before
770 * it gets passed through to fout.
771 */
772 strncpy(parsebuf, line, sizeof(parsebuf) - 1);
773 parsebuf[sizeof(parsebuf) - 1] = '\0';
774
775 /* Everything except the entry being removed passes through. */
776 if (classify_line(shr_trim(parsebuf), &in_excl,
777 &val) == EXCL_LINE_ENTRY &&
778 !removed && !strcmp(val, entry))
779 removed = true1; /* skip this line */
780 else
781 fputs(line, fout);
782 }
783
784 fclose(fin);
785
786 if (fflush(fout) != 0 || fsync(fileno(fout)) != 0) {
787 ret = -errno(*__errno_location ());
788 fclose(fout);
789 unlink(tmp);
790 return ret;
791 }
792 if (fclose(fout) != 0) {
793 ret = -errno(*__errno_location ());
794 unlink(tmp);
795 return ret;
796 }
797
798 if (!removed) {
799 unlink(tmp);
800 return -ENOENT2;
801 }
802
803 if (rename(tmp, path) < 0) {
804 ret = -errno(*__errno_location ());
805 unlink(tmp);
806 return ret;
807 }
808 shr_fsync_dir(dir); /* make the rename durable */
809 return ret;
810}
811
812/*
813 * FNV-1a 64-bit over a byte range. Used as an opaque optimistic-concurrency
814 * token: read() hands the caller the hash of the file it saw, write() refuses
815 * if the file no longer hashes to that value. Never returns 0 -- that value
816 * is reserved to mean "the list did not exist".
817 */
818static uint64_t content_hash(const char *buf, size_t len)
819{
820 uint64_t h = shr_fnv1a_64(buf, len);
821
822 return h ? h : 1;
823}
824
825/*
826 * Read the whole file at @path into a newly allocated, NUL-terminated buffer.
827 * On success sets *out (caller frees) and *len (excluding the NUL), returns 0.
828 * Returns -ENOENT if the file does not exist, or a negative errno otherwise.
829 */
830static int slurp(const char *path, char **out, size_t *len)
831{
832 struct stat st;
833 char *buf;
834 size_t off = 0;
835 int fd, ret = 0;
836
837 fd = open(path, O_RDONLY00 | O_CLOEXEC02000000);
838 if (fd < 0)
839 return -errno(*__errno_location ());
840 if (fstat(fd, &st) < 0) {
841 ret = -errno(*__errno_location ());
842 goto out;
843 }
844 if (st.st_size > EXCL_FILE_MAX(1 * 1024 * 1024)) {
845 ret = -EFBIG27;
846 goto out;
847 }
848
849 buf = malloc(st.st_size + 1);
850 if (!buf) {
851 ret = -ENOMEM12;
852 goto out;
853 }
854
855 while (off < (size_t)st.st_size) {
856 ssize_t n = read(fd, buf + off, st.st_size - off);
857
858 if (n < 0) {
859 if (errno(*__errno_location ()) == EINTR4)
860 continue;
861 free(buf);
862 ret = -errno(*__errno_location ());
863 goto out;
864 }
865 if (n == 0)
866 break;
867 off += n;
868 }
869 buf[off] = '\0';
870 *out = buf;
871 *len = off;
872out:
873 close(fd);
874 return ret;
875}
876
877/* Hash the current on-disk list. Sets *out to 0 when the list is absent. */
878static int hash_file(const char *path, uint64_t *out)
879{
880 __cleanup_free__attribute__((cleanup(shr_freep))) char *buf = NULL((void*)0);
881 size_t len;
882 int ret;
883
884 ret = slurp(path, &buf, &len);
885 if (ret == -ENOENT2) {
886 *out = 0;
887 return 0;
888 }
889 if (ret)
890 return ret;
891 *out = content_hash(buf, len);
892 return 0;
893}
894
895/*
896 * Validate every "exclusion = ..." line in @text. Comments, blank lines and
897 * non-exclusion keys are ignored. Returns 0 if all entries are valid, -EINVAL
898 * otherwise. The public write path validates here too -- it cannot trust the
899 * caller to have pre-checked the buffer. Writing is stricter than reading:
900 * a malformed section header or an entry outside [exclusions] would be
901 * silently skipped by the readers (disarming the entry), so reject the buffer
902 * loudly here instead of letting an editor persist it.
903 */
904static int validate_conf_buf(struct libnvme_global_ctx *ctx, const char *text)
905{
906 __cleanup_free__attribute__((cleanup(shr_freep))) char *copy = strdup(text);
6
Memory is allocated
907 char *save = NULL((void*)0), *line;
908 bool_Bool in_excl = false0;
909 int ret = 0;
910
911 if (!copy)
7
Assuming 'copy' is non-null
8
Taking false branch
912 return -ENOMEM12;
913
914 for (line = strtok_r(copy, "\n", &save); line;
9
Potential leak of memory pointed to by 'copy'
915 line = strtok_r(NULL((void*)0), "\n", &save)) {
916 char *s = shr_trim(line), *val;
917
918 switch (classify_line(s, &in_excl, &val)) {
919 case EXCL_LINE_ENTRY:
920 if (!entry_valid(ctx, val))
921 ret = -EINVAL22;
922 break;
923 case EXCL_LINE_STRAY:
924 case EXCL_LINE_JUNK:
925 ret = -EINVAL22;
926 break;
927 default:
928 break;
929 }
930 if (ret)
931 break;
932 }
933 return ret;
934}
935
936__shr_public__attribute__((visibility("default"))) int libnvmf_exclusion_read(struct libnvme_global_ctx *ctx,
937 const char *name, char **text,
938 uint64_t *version)
939{
940 char pathbuf[PATH_MAX4096];
941 const char *path;
942 size_t len = 0;
943 int ret;
944
945 if (!ctx)
946 return -EINVAL22;
947 if (!text || !version)
948 return -EINVAL22;
949 *text = NULL((void*)0);
950 *version = 0;
951
952 path = excl_path(ctx, name, pathbuf, sizeof(pathbuf));
953 if (!path)
954 return -EINVAL22;
955
956 ret = slurp(path, text, &len);
957 if (ret == -ENOENT2) {
958 /* A missing list reads as empty so an editor can create it. */
959 *text = strdup("");
960 return *text ? 0 : -ENOMEM12;
961 }
962 if (ret)
963 return ret;
964
965 *version = content_hash(*text, len);
966 return 0;
967}
968
969__shr_public__attribute__((visibility("default"))) int libnvmf_exclusion_write(struct libnvme_global_ctx *ctx,
970 const char *name, const char *text,
971 uint64_t version)
972{
973 char pathbuf[PATH_MAX4096], tmp[PATH_MAX4096];
974 const char *path, *dir;
975 uint64_t cur;
976 int dir_fd, fd, ret;
977
978 if (!ctx)
1
Assuming 'ctx' is non-null
2
Taking false branch
979 return -EINVAL22;
980 if (!text)
3
Assuming 'text' is non-null
4
Taking false branch
981 return -EINVAL22;
982
983 ret = validate_conf_buf(ctx, text);
5
Calling 'validate_conf_buf'
984 if (ret)
985 return ret;
986
987 ret = ensure_excl_dir(ctx, name);
988 if (ret)
989 return ret;
990
991 path = excl_path(ctx, name, pathbuf, sizeof(pathbuf));
992 if (!path)
993 return -EINVAL22;
994
995 /*
996 * Optimistic concurrency: serialize only the compare-and-swap window
997 * (recheck the on-disk version, then rename) under a directory lock.
998 * The editor ran unlocked, so two editors never block on each other --
999 * the second to save sees a changed version and gets -ESTALE rather than
1000 * silently clobbering the first.
1001 */
1002 dir = excl_dir(ctx, name);
1003 if (!dir || excl_tmp(dir, tmp, sizeof(tmp)))
1004 return -ENAMETOOLONG36;
1005 dir_fd = open(dir, O_RDONLY00 | O_DIRECTORY0200000 | O_CLOEXEC02000000);
1006 if (dir_fd < 0)
1007 return -errno(*__errno_location ());
1008 if (flock(dir_fd, LOCK_EX2) < 0) {
1009 ret = -errno(*__errno_location ());
1010 goto out;
1011 }
1012
1013 ret = hash_file(path, &cur);
1014 if (ret)
1015 goto out;
1016 if (cur != version) {
1017 ret = -ESTALE116;
1018 goto out;
1019 }
1020
1021 fd = shr_mkstemp(tmp);
1022 if (fd < 0) {
1023 ret = fd;
1024 goto out;
1025 }
1026
1027 /* mkstemp creates 0600; widen to /etc/nvme policy (world-readable). */
1028 if (fchmod(fd, 0644) < 0) {
1029 ret = -errno(*__errno_location ());
1030 goto err_tmp;
1031 }
1032 ret = shr_write_all(fd, text, strlen(text));
1033 if (ret)
1034 goto err_tmp;
1035 if (fsync(fd) < 0) {
1036 ret = -errno(*__errno_location ());
1037 goto err_tmp;
1038 }
1039 close(fd);
1040
1041 if (rename(tmp, path) < 0) {
1042 ret = -errno(*__errno_location ());
1043 unlink(tmp);
1044 goto out;
1045 }
1046 shr_fsync_dir(dir); /* make the rename durable */
1047 ret = 0;
1048 goto out;
1049
1050err_tmp:
1051 close(fd);
1052 unlink(tmp);
1053out:
1054 close(dir_fd); /* releases the flock */
1055 return ret;
1056}