Bug Summary

File:.build-ci/../libnvme/src/nvme/exclusion.c
Warning:line 900, 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 .. -I shared -I ../shared -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=gnu99 -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-08-08-045408-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 <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 */
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. Parsed silently (ctx == NULL):
218 * matching runs on every connect, so a bad hand-edited entry must stay quiet.
219 */
220static 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 */
242static 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 */
266enum 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 */
282static 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
315typedef bool_Bool (*scan_fn)(const char *entry, void *ctx);
316
317static 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
344static 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
394struct iter_ctx {
395 void (*callback)(const char *entry, void *user_data);
396 void *user_data;
397};
398
399static 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 */
639static 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 */
804static 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 */
816static 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;
858out:
859 close(fd);
860 return ret;
861}
862
863/* Hash the current on-disk list. Sets *out to 0 when the list is absent. */
864static 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 */
890static int validate_conf_buf(struct libnvme_global_ctx *ctx, const char *text)
891{
892 __cleanup_free__attribute__((cleanup(shr_freep))) char *copy = strdup(text);
6
Memory is allocated
893 char *save = NULL((void*)0), *line;
894 bool_Bool in_excl = false0;
895 int ret = 0;
896
897 if (!copy)
7
Assuming 'copy' is non-null
8
Taking false branch
898 return -ENOMEM12;
899
900 for (line = strtok_r(copy, "\n", &save); line;
9
Potential leak of memory pointed to by 'copy'
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)
1
Assuming 'ctx' is non-null
2
Taking false branch
965 return -EINVAL22;
966 if (!text)
3
Assuming 'text' is non-null
4
Taking false branch
967 return -EINVAL22;
968
969 ret = validate_conf_buf(ctx, text);
5
Calling 'validate_conf_buf'
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
1036err_tmp:
1037 close(fd);
1038 unlink(tmp);
1039out:
1040 close(dir_fd); /* releases the flock */
1041 return ret;
1042}