Bug Summary

File:.build-ci/../libnvme/src/nvme/registry.c
Warning:line 102, column 30
Potential leak of memory pointed to by 'tmp'

Annotated Source Code

Press '?' to see keyboard shortcuts

clang -cc1 -cc1 -triple x86_64-pc-linux-gnu -analyze -disable-free -clear-ast-before-backend -disable-llvm-verifier -discard-value-names -main-file-name registry.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/lib/llvm-19/lib/clang/19 -include /__w/nvme-cli/nvme-cli/.build-ci/nvme-config.h -I libnvme/src/libnvme3.so.3.0.0.p -I libnvme/src -I ../libnvme/src -I ccan -I ../ccan -I . -I .. -I /usr/include/json-c -D _FILE_OFFSET_BITS=64 -D _GNU_SOURCE -U NDEBUG -internal-isystem /usr/lib/llvm-19/lib/clang/19/include -internal-isystem /usr/local/include -internal-isystem /usr/lib/gcc/x86_64-linux-gnu/14/../../../../x86_64-linux-gnu/include -internal-externc-isystem /usr/include/x86_64-linux-gnu -internal-externc-isystem /include -internal-externc-isystem /usr/include -O3 -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 -D__GCC_HAVE_DWARF2_CFI_ASM=1 -o /__w/nvme-cli/nvme-cli/.build-ci/scan-results/2026-06-24-175442-590-1 -x c ../libnvme/src/nvme/registry.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#include <dirent.h>
10#include <errno(*__errno_location ()).h>
11#include <fcntl.h>
12#include <limits.h>
13#include <stdbool.h>
14#include <stdio.h>
15#include <stdlib.h>
16#include <string.h>
17#include <sys/stat.h>
18#include <sys/types.h>
19#include <unistd.h>
20
21#include "cleanup.h"
22#include "compiler-attributes.h"
23#include "private.h"
24#include "registry.h"
25
26/*
27 * Return the root directory of the registry. In production this is always
28 * "/run/nvme/registry". The NVME_REGISTRY_DIR override exists solely for unit
29 * testing: it lets the test suite run against a throwaway directory under /tmp
30 * instead of the production location. The result is cached on first use.
31 */
32static const char *registry_dir(void)
33{
34 static char buf[256];
35 static const char *dir;
36
37 if (!dir) {
38 const char *env = getenv("NVME_REGISTRY_DIR");
39
40 /*
41 * NVME_REGISTRY_DIR is a test-only override. The registry
42 * path drives mkdir, writes and a recursive delete, so an
43 * attacker who can inject it into a privileged process must
44 * not redirect those to an arbitrary location. Confine the
45 * override to /tmp and reject ".." traversal; anything else
46 * falls back to the production directory.
47 *
48 * Copy into a static buffer: getenv()'s pointer can be
49 * invalidated by a later setenv()/putenv(). Truncation is
50 * harmless -- a too-long test path simply won't resolve, and a
51 * malicious value is both truncated and rejected above.
52 */
53 if (env && strncmp(env, "/tmp/", 5) == 0 &&
54 !strstr(env, "..")) {
55 snprintf(buf, sizeof(buf), "%s", env);
56 dir = buf;
57 } else {
58 dir = "/run/nvme/registry";
59 }
60 }
61 return dir;
62}
63
64/*
65 * Build "<registry_dir>/<device>" (when @attr is NULL) or
66 * "<registry_dir>/<device>/<attr>". Returns a newly allocated string the
67 * caller must free, or NULL on allocation failure.
68 */
69static char *registry_path(const char *device, const char *attr)
70{
71 char *path = NULL((void*)0);
72 int n;
73
74 if (attr)
75 n = asprintf(&path, "%s/%s/%s", registry_dir(), device, attr);
76 else
77 n = asprintf(&path, "%s/%s", registry_dir(), device);
78 return n < 0 ? NULL((void*)0) : path;
79}
80
81/* mkdir -p: create @path and any missing parents. */
82static int mkdir_p(const char *path, mode_t mode)
83{
84 __cleanup_free__attribute__((cleanup(freep))) char *tmp = strdup(path);
8
Memory is allocated
85 size_t len;
86 char *p;
87
88 if (!tmp)
9
Assuming 'tmp' is non-null
10
Taking false branch
89 return -ENOMEM12;
90 len = strlen(tmp);
91 if (len && tmp[len - 1] == '/')
11
Assuming 'len' is 0
92 tmp[len - 1] = '\0';
93
94 for (p = tmp + 1; *p; p++) {
95 if (*p != '/')
96 continue;
97 *p = '\0';
98 if (mkdir(tmp, mode) < 0 && errno(*__errno_location ()) != EEXIST17)
99 return -errno(*__errno_location ());
100 *p = '/';
101 }
102 if (mkdir(tmp, mode) < 0 && errno(*__errno_location ()) != EEXIST17)
12
Potential leak of memory pointed to by 'tmp'
103 return -errno(*__errno_location ());
104 return 0;
105}
106
107static int ensure_device_dir(const char *device)
108{
109 __cleanup_free__attribute__((cleanup(freep))) char *path = NULL((void*)0);
110 int ret;
111
112 ret = mkdir_p(registry_dir(), 0755);
7
Calling 'mkdir_p'
113 if (ret)
114 return ret;
115
116 path = registry_path(device, NULL((void*)0));
117 if (!path)
118 return -ENOMEM12;
119
120 /* EEXIST is success: two concurrent creates race to the same result. */
121 if (mkdir(path, 0755) < 0 && errno(*__errno_location ()) != EEXIST17)
122 return -errno(*__errno_location ());
123 return 0;
124}
125
126static bool_Bool valid_device(const char *device)
127{
128 const char *p;
129
130 if (!device || strncmp(device, "nvme", 4) != 0)
131 return false0;
132 p = device + 4;
133 if (!*p)
134 return false0;
135 for (; *p; p++) {
136 if (*p < '0' || *p > '9')
137 return false0;
138 }
139 return true1;
140}
141
142static bool_Bool valid_attr(const char *attr)
143{
144 const char *p;
145
146 if (!attr || !*attr)
147 return false0;
148 for (p = attr; *p; p++) {
149 if ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') ||
150 (*p >= '0' && *p <= '9') || *p == '_' || *p == '-')
151 continue;
152 return false0;
153 }
154 return true1;
155}
156
157static int write_all(int fd, const char *buf, size_t len)
158{
159 while (len) {
160 ssize_t w = write(fd, buf, len);
161
162 if (w < 0) {
163 if (errno(*__errno_location ()) == EINTR4)
164 continue;
165 return -errno(*__errno_location ());
166 }
167 if (w == 0)
168 return -EIO5;
169 buf += w;
170 len -= w;
171 }
172 return 0;
173}
174
175/*
176 * read() up to @count bytes into @buf, retrying on EINTR. Returns the number
177 * of bytes read (possibly short, at EOF) or a negative errno.
178 */
179static ssize_t read_full(int fd, char *buf, size_t count)
180{
181 size_t off = 0;
182
183 while (off < count) {
184 ssize_t r = read(fd, buf + off, count - off);
185
186 if (r < 0) {
187 if (errno(*__errno_location ()) == EINTR4)
188 continue;
189 return -errno(*__errno_location ());
190 }
191 if (r == 0)
192 break;
193 off += r;
194 }
195 return off;
196}
197
198/*
199 * Read the attribute file open at @fd into a newly allocated, NUL-terminated
200 * string with the trailing newline stripped. On success sets *out (the caller
201 * frees) and returns 0. Returns -ENOENT for an empty file, or a negative
202 * errno on failure.
203 */
204static int read_attr_fd(int fd, char **out)
205{
206 struct stat st;
207 char *val;
208 ssize_t n;
209
210 if (fstat(fd, &st) < 0)
211 return -errno(*__errno_location ());
212 if (st.st_size == 0)
213 return -ENOENT2;
214
215 val = malloc(st.st_size + 1);
216 if (!val)
217 return -ENOMEM12;
218
219 n = read_full(fd, val, st.st_size);
220 if (n < 0) {
221 free(val);
222 return n;
223 }
224
225 while (n > 0 && (val[n - 1] == '\n' || val[n - 1] == '\r'))
226 n--;
227 val[n] = '\0';
228 *out = val;
229 return 0;
230}
231
232/*
233 * Write @value atomically to the attribute file @attr inside @dir_path.
234 *
235 * Protocol:
236 * mkostemp -> <dir_path>/<attr>.tmp.XXXXXX (random name, O_CLOEXEC)
237 * fchmod -> 0644
238 * write -> value + newline
239 * rename -> <dir_path>/<attr>
240 * fsync -> directory
241 *
242 * mkostemp() atomically creates the tmp file with a random suffix, preventing
243 * both name prediction and TOCTOU races on the tmp file itself. Builds without
244 * _GNU_SOURCE fall back to mkstemp() + fcntl(FD_CLOEXEC) (see below). A NULL
245 * @value removes the attribute.
246 */
247static int write_attr_atomic(int dir_fd, const char *dir_path,
248 const char *attr, const char *value)
249{
250 __cleanup_free__attribute__((cleanup(freep))) char *tmp_path = NULL((void*)0);
251 __cleanup_free__attribute__((cleanup(freep))) char *final_path = NULL((void*)0);
252 int fd, ret;
253
254 if (!valid_attr(attr))
255 return -EINVAL22;
256
257 if (!value) {
258 if (unlinkat(dir_fd, attr, 0) < 0 && errno(*__errno_location ()) != ENOENT2)
259 return -errno(*__errno_location ());
260 fsync(dir_fd);
261 return 0;
262 }
263
264 if (asprintf(&final_path, "%s/%s", dir_path, attr) < 0)
265 return -ENOMEM12;
266 if (asprintf(&tmp_path, "%s/%s.tmp.XXXXXX", dir_path, attr) < 0)
267 return -ENOMEM12;
268
269 /*
270 * mkostemp() sets O_CLOEXEC atomically but its glibc declaration is
271 * gated behind _GNU_SOURCE; fall back to mkstemp() + fcntl() where
272 * _GNU_SOURCE is not defined (e.g. the musl-style CI build).
273 */
274#ifdef _GNU_SOURCE1
275 fd = mkostemp(tmp_path, O_CLOEXEC02000000);
276 if (fd < 0)
277 return -errno(*__errno_location ());
278#else
279 fd = mkstemp(tmp_path);
280 if (fd < 0)
281 return -errno(*__errno_location ());
282 if (fcntl(fd, F_SETFD2, FD_CLOEXEC1) < 0) {
283 ret = -errno(*__errno_location ());
284 goto err;
285 }
286#endif
287
288 /* the temp file is created with 0600; open it to the registry world. */
289 if (fchmod(fd, 0644) < 0) {
290 ret = -errno(*__errno_location ());
291 goto err;
292 }
293
294 ret = write_all(fd, value, strlen(value));
295 if (ret)
296 goto err;
297 ret = write_all(fd, "\n", 1);
298 if (ret)
299 goto err;
300 close(fd);
301
302 if (rename(tmp_path, final_path) < 0) {
303 ret = -errno(*__errno_location ());
304 unlink(tmp_path);
305 return ret;
306 }
307
308 /* Flush the rename to stable storage. */
309 fsync(dir_fd);
310 return 0;
311
312err:
313 close(fd);
314 unlink(tmp_path);
315 return ret;
316}
317
318/*
319 * Open the directory for @device (creating it if needed) and write @attr=@value
320 * atomically. Shared by the create and update paths.
321 */
322static int write_device_attr(const char *device, const char *attr,
323 const char *value)
324{
325 __cleanup_free__attribute__((cleanup(freep))) char *dir_path = NULL((void*)0);
326 int dir_fd, ret;
327
328 ret = ensure_device_dir(device);
6
Calling 'ensure_device_dir'
329 if (ret)
330 return ret;
331
332 dir_path = registry_path(device, NULL((void*)0));
333 if (!dir_path)
334 return -ENOMEM12;
335
336 dir_fd = open(dir_path, O_RDONLY00 | O_DIRECTORY0200000 | O_CLOEXEC02000000);
337 if (dir_fd < 0)
338 return -errno(*__errno_location ());
339
340 ret = write_attr_atomic(dir_fd, dir_path, attr, value);
341 close(dir_fd);
342 return ret;
343}
344
345/*
346 * libnvmf_registry_create_instance - Write a registry entry for a freshly
347 * connected controller. Internal; called from the connect path in fabrics.c
348 * once the kernel returns instance=N. Always overwrites any pre-existing
349 * entry: instance recycling means an old nvmeN/ directory is stale by
350 * definition.
351 */
352int libnvmf_registry_create_instance(struct libnvme_global_ctx *ctx,
353 int instance, const char *owner)
354{
355 char device[32];
356
357 snprintf(device, sizeof(device), "nvme%d", instance);
358
359 /*
360 * Delete any stale entry unconditionally before creating the new one.
361 * Instance recycling means an old nvmeN/ directory is stale by
362 * definition — any attributes left over from the previous owner (e.g.
363 * a private attribute written via libnvmf_registry_update()) must not
364 * leak into the new entry. Ignore errors: ENOENT is the common case.
365 */
366 libnvmf_registry_delete(ctx, device);
367
368 return write_device_attr(device, "owner", owner);
369}
370
371int libnvmf_registry_delete_instance(struct libnvme_global_ctx *ctx,
372 int instance)
373{
374 char device[32];
375 int ret;
376
377 snprintf(device, sizeof(device), "nvme%d", instance);
378 ret = libnvmf_registry_delete(ctx, device);
379 return (ret == -ENOENT2) ? 0 : ret;
380}
381
382__libnvme_public__attribute__((visibility("default"))) int libnvmf_registry_retrieve(struct libnvme_global_ctx *ctx,
383 const char *device,
384 const char *attr, char **value)
385{
386 __cleanup_free__attribute__((cleanup(freep))) char *path = NULL((void*)0);
387 int fd, ret;
388
389 if (!ctx)
390 return -EINVAL22;
391 if (!device || !attr || !value)
392 return -EINVAL22;
393 if (!valid_device(device))
394 return -EINVAL22;
395
396 path = registry_path(device, attr);
397 if (!path)
398 return -ENOMEM12;
399
400 fd = open(path, O_RDONLY00 | O_CLOEXEC02000000);
401 if (fd < 0)
402 return -errno(*__errno_location ());
403
404 ret = read_attr_fd(fd, value);
405 close(fd);
406 return ret;
407}
408
409__libnvme_public__attribute__((visibility("default"))) int libnvmf_registry_attr_equal(struct libnvme_global_ctx *ctx,
410 const char *device,
411 const char *attr,
412 const char *value)
413{
414 char *stored = NULL((void*)0);
415 int rc;
416
417 if (!ctx)
418 return -EINVAL22;
419
420 rc = libnvmf_registry_retrieve(ctx, device, attr, &stored);
421 if (rc < 0 && rc != -ENOENT2)
422 return rc;
423 if (!stored)
424 rc = value ? 1 : 0;
425 else
426 rc = (value && !strcmp(stored, value)) ? 0 : 1;
427 free(stored);
428 return rc;
429}
430
431__libnvme_public__attribute__((visibility("default"))) int libnvmf_registry_update(struct libnvme_global_ctx *ctx,
432 const char *device,
433 const char *attr, const char *value)
434{
435 if (!ctx)
1
Assuming 'ctx' is non-null
436 return -EINVAL22;
437 if (!device || !valid_device(device))
2
Assuming 'device' is non-null
3
Assuming the condition is false
4
Taking false branch
438 return -EINVAL22;
439
440 return write_device_attr(device, attr, value);
5
Calling 'write_device_attr'
441}
442
443__libnvme_public__attribute__((visibility("default"))) int libnvmf_registry_delete(struct libnvme_global_ctx *ctx,
444 const char *device)
445{
446 __cleanup_free__attribute__((cleanup(freep))) char *path = NULL((void*)0);
447 struct dirent *de;
448 int dir_fd, ret = 0;
449 DIR *d;
450
451 if (!ctx)
452 return -EINVAL22;
453 if (!device || !valid_device(device))
454 return -EINVAL22;
455
456 path = registry_path(device, NULL((void*)0));
457 if (!path)
458 return -ENOMEM12;
459
460 d = opendir(path);
461 if (!d)
462 return -errno(*__errno_location ());
463
464 dir_fd = dirfd(d);
465 while ((de = readdir(d)) != NULL((void*)0)) {
466 if (de->d_name[0] == '.')
467 continue;
468 if (unlinkat(dir_fd, de->d_name, 0) < 0 && errno(*__errno_location ()) != ENOENT2)
469 ret = -errno(*__errno_location ());
470 }
471 closedir(d);
472
473 if (ret)
474 return ret;
475
476 if (rmdir(path) < 0 && errno(*__errno_location ()) != ENOENT2)
477 return -errno(*__errno_location ());
478 return 0;
479}
480
481__libnvme_public__attribute__((visibility("default"))) int libnvmf_registry_device_for_each(
482 struct libnvme_global_ctx *ctx,
483 void (*callback)(const char *device, void *user_data),
484 void *user_data)
485{
486 char dev_path[NAME_MAX255 + 6]; /* "/dev/" + name + NUL */
487 struct dirent *de;
488 DIR *d;
489
490 if (!ctx)
491 return -EINVAL22;
492 if (!callback)
493 return -EINVAL22;
494
495 d = opendir(registry_dir());
496 if (!d) {
497 if (errno(*__errno_location ()) == ENOENT2)
498 return 0;
499 return -errno(*__errno_location ());
500 }
501
502 while ((de = readdir(d)) != NULL((void*)0)) {
503 if (de->d_name[0] == '.')
504 continue;
505
506 /*
507 * Only visit directories. Use stat() as a fallback when
508 * d_type is DT_UNKNOWN (e.g. on some network filesystems).
509 */
510 if (de->d_type != DT_DIRDT_DIR) {
511 struct stat st;
512
513 if (de->d_type != DT_UNKNOWNDT_UNKNOWN)
514 continue;
515 /*
516 * Resolve relative to the open directory rather than
517 * building an absolute path: avoids a fixed-size path
518 * buffer (and the format-truncation it invites).
519 */
520 if (fstatat(dirfd(d), de->d_name, &st, 0) < 0 ||
521 !S_ISDIR(st.st_mode)((((st.st_mode)) & 0170000) == (0040000)))
522 continue;
523 }
524
525 /* Stale-entry check: skip if the device node is gone. */
526 snprintf(dev_path, sizeof(dev_path), "/dev/%s", de->d_name);
527 if (access(dev_path, F_OK0) != 0)
528 continue;
529
530 callback(de->d_name, user_data);
531 }
532 closedir(d);
533 return 0;
534}
535
536__libnvme_public__attribute__((visibility("default"))) int libnvmf_registry_attr_for_each(
537 struct libnvme_global_ctx *ctx,
538 const char *device,
539 void (*callback)(const char *attr, const char *value,
540 void *user_data),
541 void *user_data)
542{
543 __cleanup_free__attribute__((cleanup(freep))) char *dir_path = NULL((void*)0);
544 struct dirent *de;
545 int dir_fd;
546 DIR *d;
547
548 if (!ctx)
549 return -EINVAL22;
550 if (!device || !callback)
551 return -EINVAL22;
552 if (!valid_device(device))
553 return -EINVAL22;
554
555 dir_path = registry_path(device, NULL((void*)0));
556 if (!dir_path)
557 return -ENOMEM12;
558
559 d = opendir(dir_path);
560 if (!d)
561 return -errno(*__errno_location ());
562
563 dir_fd = dirfd(d);
564 while ((de = readdir(d)) != NULL((void*)0)) {
565 __cleanup_free__attribute__((cleanup(freep))) char *val = NULL((void*)0);
566 int fd, rc;
567
568 if (de->d_name[0] == '.')
569 continue;
570 /* Skip in-flight tmp files from concurrent writers. */
571 if (strstr(de->d_name, ".tmp."))
572 continue;
573
574 fd = openat(dir_fd, de->d_name, O_RDONLY00 | O_CLOEXEC02000000);
575 if (fd < 0)
576 continue; /* device may have been removed concurrently */
577
578 rc = read_attr_fd(fd, &val);
579 close(fd);
580 if (rc == 0)
581 callback(de->d_name, val, user_data);
582 }
583 closedir(d);
584 return 0;
585}