Coverage Report

Created: 2026-10-28 07:10

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/dovecot/src/lib/path-util.c
Line
Count
Source
1
/* Copyright (c) Dovecot authors, see top-level COPYING file */
2
3
#include "lib.h"
4
#include "str.h"
5
#include "path-util.h"
6
7
#include <fcntl.h>
8
#include <unistd.h>
9
#include <sys/types.h>
10
#include <sys/stat.h>
11
12
0
#define PATH_UTIL_MAX_PATH      8*1024
13
0
#define PATH_UTIL_MAX_SYMLINKS  80
14
/* Symlink-hop cap for t_openat_safe(). Lower than the realpath cap:
15
   t_openat_safe() refuses absolute and escaping targets outright, so a
16
   long chain inside the base dir is the only legitimate use. */
17
0
#define PATH_UTIL_OPENAT_SAFE_MAX_SYMLINKS 16
18
19
static int t_getcwd_noalloc(char **dir_r, size_t *asize_r,
20
          const char **error_r) ATTR_NULL(2)
21
0
{
22
  /* @UNSAFE */
23
0
  char *dir;
24
0
  size_t asize = 128;
25
26
0
  dir = t_buffer_get(asize);
27
0
  while (getcwd(dir, asize) == NULL) {
28
0
    if (errno != ERANGE) {
29
0
      *error_r = t_strdup_printf("getcwd() failed: %m");
30
0
      return -1;
31
0
    }
32
0
    asize = nearest_power(asize+1);
33
0
    dir = t_buffer_get(asize);
34
0
  }
35
0
  if (asize_r != NULL)
36
0
    *asize_r = asize;
37
0
  *dir_r = dir;
38
0
  return 0;
39
0
}
40
41
static int path_normalize(const char *path, bool resolve_links,
42
        const char **npath_r, const char **error_r)
43
0
{
44
  /* @UNSAFE */
45
0
  unsigned int link_count = 0;
46
0
  char *npath, *npath_pos;
47
0
  const char *p;
48
0
  size_t asize;
49
50
0
  i_assert(path != NULL);
51
0
  i_assert(npath_r != NULL);
52
0
  i_assert(error_r != NULL);
53
54
0
  if (path[0] != '/') {
55
    /* relative; initialize npath with current directory */
56
0
    if (t_getcwd_noalloc(&npath, &asize, error_r) < 0)
57
0
      return -1;
58
0
    npath_pos = npath + strlen(npath);
59
0
    i_assert(npath[0] == '/');
60
0
  } else {
61
    /* absolute; initialize npath with root */
62
0
    asize = 128;
63
0
    npath = t_buffer_get(asize);
64
0
    npath[0] = '/';
65
0
    npath_pos = npath + 1;
66
0
  }
67
68
0
  p = path;
69
0
  while (*p != '\0') {
70
0
    struct stat st;
71
0
    ptrdiff_t seglen;
72
0
    const char *segend;
73
74
    /* skip duplicate slashes */
75
0
    while (*p == '/')
76
0
      p++;
77
78
    /* find end of path segment */
79
0
    for (segend = p; *segend != '\0' && *segend != '/'; segend++);
80
81
0
    if (segend == p)
82
0
      break; /* '\0' */
83
0
    seglen = segend - p;
84
0
    if (seglen == 1 && p[0] == '.') {
85
      /* a reference to this segment; nothing to do */
86
0
    } else if (seglen == 2 && p[0] == '.' && p[1] == '.') {
87
      /* a reference to parent segment; back up to previous
88
       * slash */
89
0
      i_assert(npath_pos >= npath);
90
0
      if ((npath_pos - npath) > 1) {
91
0
        if (*(npath_pos-1) == '/')
92
0
          npath_pos--;
93
0
        for (; *(npath_pos-1) != '/'; npath_pos--);
94
0
      }
95
0
    } else {
96
      /* allocate space if necessary */
97
0
      i_assert(npath_pos >= npath);
98
0
      if ((size_t)((npath_pos - npath) + seglen + 1) >= asize) {
99
0
        ptrdiff_t npath_offset = npath_pos - npath;
100
0
        asize = nearest_power(MALLOC_ADD3(npath_offset, seglen, 2));
101
0
        npath = t_buffer_reget(npath, asize);
102
0
        npath_pos = npath + npath_offset;
103
0
      }
104
105
      /* make sure npath now ends in slash */
106
0
      i_assert(npath_pos > npath);
107
0
      if (*(npath_pos-1) != '/') {
108
0
        i_assert((size_t)((npath_pos - npath) + 1) < asize);
109
0
        *(npath_pos++) = '/';
110
0
      }
111
112
      /* copy segment to normalized path */
113
0
      i_assert(npath_pos >= npath);
114
0
      i_assert((size_t)((npath_pos - npath) + seglen) < asize);
115
0
      memmove(npath_pos, p, seglen);
116
0
      npath_pos += seglen;
117
0
    }
118
119
0
    if (resolve_links) {
120
      /* stat path up to here (segend points to tail) */
121
0
      *npath_pos = '\0';
122
0
      if (lstat(npath, &st) < 0) {
123
0
        *error_r = t_strdup_printf("lstat() failed: %m");
124
0
        return -1;
125
0
      }
126
127
0
      if (S_ISLNK (st.st_mode)) {
128
        /* symlink */
129
0
        char *npath_link;
130
0
        size_t lsize = 128, tlen = strlen(segend), espace;
131
0
        size_t ltlen = (link_count == 0 ? 0 : tlen);
132
0
        ssize_t ret;
133
134
        /* limit link dereferences */
135
0
        if (++link_count > PATH_UTIL_MAX_SYMLINKS) {
136
0
          errno = ELOOP;
137
0
          *error_r = "Too many symlink dereferences";
138
0
          return -1;
139
0
        }
140
141
        /* allocate space for preserving tail of previous symlink and
142
           first attempt at reading symlink with room for the tail
143
144
           buffer will look like this:
145
           [npath][0][preserved tail][link buffer][room for tail][0]
146
         */
147
0
        espace = ltlen + tlen + 2;
148
0
        i_assert(npath_pos >= npath);
149
0
        if ((size_t)((npath_pos - npath) + espace + lsize) >= asize) {
150
0
          ptrdiff_t npath_offset = npath_pos - npath;
151
0
          asize = nearest_power(MALLOC_ADD3(npath_offset, espace, lsize) + 1);
152
0
          lsize = asize - (npath_offset + espace);
153
0
          npath = t_buffer_reget(npath, asize);
154
0
          npath_pos = npath + npath_offset;
155
0
        }
156
157
0
        if (ltlen > 0) {
158
          /* preserve tail just after end of npath */
159
0
          i_assert(npath_pos >= npath);
160
0
          i_assert((size_t)((npath_pos + 1 - npath) + ltlen) < asize);
161
0
          memmove(npath_pos + 1, segend, ltlen);
162
0
        }
163
164
        /* read the symlink after the preserved tail */
165
0
        for (;;) {
166
0
          npath_link = (npath_pos + 1) + ltlen;
167
168
0
          i_assert(npath_link >= npath_pos);
169
0
          i_assert((size_t)((npath_link - npath) + lsize) < asize);
170
171
          /* attempt to read the link */
172
0
          if ((ret=readlink(npath, npath_link, lsize)) < 0) {
173
0
            *error_r = t_strdup_printf("readlink() failed: %m");
174
0
            return -1;
175
0
          }
176
0
          if ((size_t)ret < lsize) {
177
            /* POSIX doesn't guarantee the presence of a NIL */
178
0
            npath_link[ret] = '\0';
179
0
            break;
180
0
          }
181
182
          /* sum of new symlink content length
183
           * and path tail length may not
184
             exceed maximum */
185
0
          if ((size_t)(ret + tlen) >= PATH_UTIL_MAX_PATH) {
186
0
            errno = ENAMETOOLONG;
187
0
            *error_r = "Resulting path is too long";
188
0
            return -1;
189
0
          }
190
191
          /* try again with bigger buffer,
192
             we need to allocate more space as well if lsize == ret,
193
             because the returned link may have gotten truncated */
194
0
          espace = ltlen + tlen + 2;
195
0
          i_assert(npath_pos >= npath);
196
0
          if ((size_t)((npath_pos - npath) + espace + lsize) >= asize ||
197
0
              lsize == (size_t)ret) {
198
0
            ptrdiff_t npath_offset = npath_pos - npath;
199
0
            asize = nearest_power(MALLOC_ADD3(npath_offset, espace, lsize) + 1);
200
0
            lsize = asize - (npath_offset + espace);
201
0
            npath = t_buffer_reget(npath, asize);
202
0
            npath_pos = npath + npath_offset;
203
0
          }
204
0
        }
205
206
        /* add tail of previous path at end of symlink */
207
0
        i_assert(npath_link >= npath);
208
0
        if (ltlen > 0) {
209
0
          i_assert(npath_pos >= npath);
210
0
          i_assert((size_t)((npath_pos - npath) + 1 + tlen) < asize);
211
0
          i_assert((size_t)((npath_link - npath) + ret + tlen) < asize);
212
0
          memcpy(npath_link + ret, npath_pos + 1, tlen);
213
0
        } else {
214
0
          i_assert((size_t)((npath_link - npath) + ret + tlen) < asize);
215
0
          memcpy(npath_link + ret, segend, tlen);
216
0
        }
217
0
        *(npath_link+ret+tlen) = '\0';
218
219
        /* use as new source path */
220
0
        path = segend = npath_link;
221
222
0
        if (path[0] == '/') {
223
          /* absolute symlink; start over at root */
224
0
          npath_pos = npath + 1;
225
0
        } else {
226
          /* relative symlink; back up to previous segment */
227
0
          i_assert(npath_pos >= npath);
228
0
          if ((npath_pos - npath) > 1) {
229
0
            if (*(npath_pos-1) == '/')
230
0
              npath_pos--;
231
0
            for (; *(npath_pos-1) != '/'; npath_pos--);
232
0
          }
233
0
        }
234
235
0
      } else if (*segend != '\0' && !S_ISDIR (st.st_mode)) {
236
        /* not last segment, but not a directory either */
237
0
        errno = ENOTDIR;
238
0
        *error_r = t_strdup_printf("Not a directory: %s", npath);
239
0
        return -1;
240
0
      }
241
0
    }
242
243
0
    p = segend;
244
0
  }
245
246
0
  i_assert(npath_pos >= npath);
247
0
  i_assert((size_t)(npath_pos - npath) < asize);
248
249
  /* remove any trailing slash */
250
0
  if ((npath_pos - npath) > 1 && *(npath_pos-1) == '/')
251
0
    npath_pos--;
252
0
  *npath_pos = '\0';
253
254
0
  t_buffer_alloc(npath_pos - npath + 1);
255
0
  *npath_r = npath;
256
0
  return 0;
257
0
}
258
259
int t_normpath(const char *path, const char **npath_r, const char **error_r)
260
0
{
261
0
  return path_normalize(path, FALSE, npath_r, error_r);
262
0
}
263
264
int t_normpath_to(const char *path, const char *root, const char **npath_r,
265
      const char **error_r)
266
0
{
267
0
  i_assert(path != NULL);
268
0
  i_assert(root != NULL);
269
0
  i_assert(npath_r != NULL);
270
271
0
  if (*path == '/')
272
0
    return t_normpath(path, npath_r, error_r);
273
274
0
  return t_normpath(t_strconcat(root, "/", path, NULL), npath_r, error_r);
275
0
}
276
277
int t_realpath(const char *path, const char **npath_r, const char **error_r)
278
0
{
279
0
  return path_normalize(path, TRUE, npath_r, error_r);
280
0
}
281
282
int t_realpath_to(const char *path, const char *root, const char **npath_r,
283
      const char **error_r)
284
0
{
285
0
  i_assert(path != NULL);
286
0
  i_assert(root != NULL);
287
0
  i_assert(npath_r != NULL);
288
289
0
  if (*path == '/')
290
0
    return t_realpath(path, npath_r, error_r);
291
292
0
  return t_realpath(t_strconcat(root, "/", path, NULL), npath_r, error_r);
293
0
}
294
295
int t_abspath(const char *path, const char **abspath_r, const char **error_r)
296
0
{
297
0
  i_assert(path != NULL);
298
0
  i_assert(abspath_r != NULL);
299
0
  i_assert(error_r != NULL);
300
301
0
  if (*path == '/') {
302
0
    *abspath_r = path;
303
0
    return 0;
304
0
  }
305
306
0
  const char *dir, *error;
307
0
  if (t_get_working_dir(&dir, &error) < 0) {
308
0
    *error_r = t_strconcat("Failed to get working directory: ",
309
0
               error, NULL);
310
0
    return -1;
311
0
  }
312
0
  *abspath_r = t_strconcat(dir, "/", path, NULL);
313
0
  return 0;
314
0
}
315
316
const char *t_abspath_to(const char *path, const char *root)
317
0
{
318
0
  i_assert(path != NULL);
319
0
  i_assert(root != NULL);
320
321
0
  if (*path == '/')
322
0
    return path;
323
324
0
  return t_strconcat(root, "/", path, NULL);
325
0
}
326
327
int t_get_working_dir(const char **dir_r, const char **error_r)
328
0
{
329
0
  char *dir;
330
331
0
  i_assert(dir_r != NULL);
332
0
  i_assert(error_r != NULL);
333
0
  if (t_getcwd_noalloc(&dir, NULL, error_r) < 0)
334
0
    return -1;
335
336
0
  t_buffer_alloc(strlen(dir) + 1);
337
0
  *dir_r = dir;
338
0
  return 0;
339
0
}
340
341
int t_readlink(const char *path, const char **dest_r, const char **error_r)
342
0
{
343
0
  i_assert(error_r != NULL);
344
345
  /* @UNSAFE */
346
0
  ssize_t ret;
347
0
  char *dest;
348
0
  size_t size = 128;
349
350
0
  dest = t_buffer_get(size);
351
0
  while ((ret = readlink(path, dest, size)) >= (ssize_t)size) {
352
0
    size = nearest_power(size+1);
353
0
    dest = t_buffer_get(size);
354
0
  }
355
0
  if (ret < 0) {
356
0
    *error_r = t_strdup_printf("readlink() failed: %m");
357
0
    return -1;
358
0
  }
359
360
0
  dest[ret] = '\0';
361
0
  t_buffer_alloc(ret + 1);
362
0
  *dest_r = dest;
363
0
  return 0;
364
0
}
365
366
/* Iteratively resolve `path` beneath `cur_fd` using openat() with
367
   O_NOFOLLOW per component. Symlinks are resolved manually: their targets
368
   are validated (non-absolute, no '..' past the base) and the loop
369
   restarts with the target spliced into the remaining path. dir_depth
370
   tracks how many directory levels we have descended below the original
371
   base; '..' is refused when the depth would go negative.
372
 */
373
static int
374
path_openat_safe_walk(int cur_fd, const char *path, int flags,
375
          const char **error_r)
376
0
{
377
0
  bool cur_owned = FALSE;
378
0
  unsigned int symlink_count = 0;
379
0
  unsigned int dir_depth = 0;
380
381
  /* Absolute paths cannot stay beneath the base directory. */
382
0
  if (*path == '/') {
383
0
    errno = ELOOP;
384
0
    *error_r = "Path is absolute (escapes base directory)";
385
0
    return -1;
386
0
  }
387
388
0
  for (;;) {
389
0
    while (*path == '/')
390
0
      path++;
391
392
0
    if (*path == '\0') {
393
      /* Empty path or trailing slash: open the current
394
         directory itself with the requested flags. */
395
0
      int fd = openat(cur_fd, ".",
396
0
          flags | O_NOFOLLOW | O_CLOEXEC);
397
0
      if (cur_owned)
398
0
        i_close_fd(&cur_fd);
399
0
      if (fd < 0) {
400
0
        *error_r = t_strdup_printf(
401
0
          "openat() failed: %m");
402
0
      }
403
0
      return fd;
404
0
    }
405
406
0
    const char *slash = strchr(path, '/');
407
0
    size_t comp_len = (slash != NULL ?
408
0
           (size_t)(slash - path) : strlen(path));
409
0
    bool is_last = (slash == NULL);
410
411
0
    if (comp_len == 1 && path[0] == '.') {
412
0
      path += comp_len;
413
0
      continue;
414
0
    }
415
416
0
    if (comp_len == 2 && path[0] == '.' && path[1] == '.') {
417
0
      if (dir_depth == 0) {
418
0
        if (cur_owned)
419
0
          i_close_fd(&cur_fd);
420
0
        errno = ELOOP;
421
0
        *error_r = "Path escapes base directory via '..'";
422
0
        return -1;
423
0
      }
424
0
      int parent_fd = openat(cur_fd, "..",
425
0
                 O_RDONLY | O_DIRECTORY |
426
0
                 O_NOFOLLOW | O_CLOEXEC);
427
0
      if (cur_owned)
428
0
        i_close_fd(&cur_fd);
429
0
      if (parent_fd < 0) {
430
0
        *error_r = t_strdup_printf(
431
0
          "openat(..) failed: %m");
432
0
        return -1;
433
0
      }
434
0
      cur_fd = parent_fd;
435
0
      cur_owned = TRUE;
436
0
      dir_depth--;
437
0
      path += comp_len;
438
0
      continue;
439
0
    }
440
441
0
    const char *comp = t_strdup_until(path, path + comp_len);
442
0
    int next_fd;
443
0
    if (is_last) {
444
0
      next_fd = openat(cur_fd, comp,
445
0
           flags | O_NOFOLLOW | O_CLOEXEC);
446
0
    } else {
447
0
      next_fd = openat(cur_fd, comp,
448
0
           O_RDONLY | O_DIRECTORY |
449
0
           O_NOFOLLOW | O_CLOEXEC);
450
0
    }
451
452
0
    if (next_fd < 0 && (errno == ELOOP || errno == EMLINK)) {
453
      /* Component is a symlink. Resolve it ourselves and
454
         restart processing with its target spliced into
455
         `path`; that way the target is itself walked
456
         component-by-component and the boundary check
457
         continues to apply. (O_NOFOLLOW on a symlink reports
458
         ELOOP on Linux/macOS but EMLINK on the BSDs, so both
459
         must be treated as "this is a symlink".) */
460
0
      if (++symlink_count >
461
0
          PATH_UTIL_OPENAT_SAFE_MAX_SYMLINKS) {
462
0
        if (cur_owned)
463
0
          i_close_fd(&cur_fd);
464
0
        errno = ELOOP;
465
0
        *error_r = "Too many symlink dereferences";
466
0
        return -1;
467
0
      }
468
469
0
      char buf[PATH_UTIL_MAX_PATH];
470
0
      ssize_t llen = readlinkat(cur_fd, comp,
471
0
              buf, sizeof(buf) - 1);
472
0
      if (llen < 0) {
473
0
        if (cur_owned)
474
0
          i_close_fd(&cur_fd);
475
0
        *error_r = t_strdup_printf(
476
0
          "readlinkat(%s) failed: %m", comp);
477
0
        return -1;
478
0
      }
479
0
      buf[llen] = '\0';
480
481
0
      if (buf[0] == '/') {
482
0
        if (cur_owned)
483
0
          i_close_fd(&cur_fd);
484
0
        errno = ELOOP;
485
0
        *error_r = t_strdup_printf(
486
0
          "Symlink '%s' has absolute target "
487
0
          "'%s' (escapes base directory)",
488
0
          comp, buf);
489
0
        return -1;
490
0
      }
491
492
0
      path = is_last ? t_strdup(buf) :
493
0
           t_strconcat(buf, "/",
494
0
                 slash + 1, NULL);
495
0
      continue;
496
0
    }
497
498
0
    if (next_fd < 0) {
499
0
      if (cur_owned)
500
0
        i_close_fd(&cur_fd);
501
0
      *error_r = t_strdup_printf(
502
0
        "openat(%s) failed: %m", comp);
503
0
      return -1;
504
0
    }
505
506
0
    if (is_last) {
507
0
      if (cur_owned)
508
0
        i_close_fd(&cur_fd);
509
0
      return next_fd;
510
0
    }
511
512
0
    if (cur_owned)
513
0
      i_close_fd(&cur_fd);
514
0
    cur_fd = next_fd;
515
0
    cur_owned = TRUE;
516
0
    dir_depth++;
517
0
    path = slash + 1;
518
0
  }
519
0
}
520
521
int t_openat_safe(int base_fd, const char *path, int flags,
522
      const char **error_r)
523
0
{
524
0
  i_assert(base_fd >= 0);
525
0
  i_assert(path != NULL);
526
0
  i_assert(error_r != NULL);
527
528
0
  return path_openat_safe_walk(base_fd, path, flags, error_r);
529
0
}
530
531
int t_openat_safe_dir(const char *base_dir, const char *path, int flags,
532
          const char **error_r)
533
0
{
534
0
  i_assert(base_dir != NULL);
535
0
  i_assert(path != NULL);
536
0
  i_assert(error_r != NULL);
537
538
0
  int dir_fd = open(base_dir,
539
0
        O_DIRECTORY | O_RDONLY | O_CLOEXEC |
540
0
        (flags & O_NOFOLLOW));
541
0
  if (dir_fd < 0) {
542
0
    *error_r = t_strdup_printf("open(%s) failed: %m", base_dir);
543
0
    return -1;
544
0
  }
545
0
  int fd = t_openat_safe(dir_fd, path, flags, error_r);
546
0
  i_close_fd(&dir_fd);
547
0
  return fd;
548
0
}
549
550
bool path_has_parent_component(const char *path)
551
0
{
552
0
  const char *p = path;
553
554
0
  while ((p = strstr(p, "..")) != NULL) {
555
0
    if ((p == path || p[-1] == '/') &&
556
0
        (p[2] == '\0' || p[2] == '/'))
557
0
      return TRUE;
558
0
    p += 2;
559
0
  }
560
0
  return FALSE;
561
0
}
562
563
bool t_binary_abspath(const char **binpath, const char **error_r)
564
0
{
565
0
  const char *path_env, *const *paths;
566
0
  string_t *path;
567
568
0
  if (**binpath == '/') {
569
    /* already have absolute path */
570
0
    return TRUE;
571
0
  } else if (strchr(*binpath, '/') != NULL) {
572
    /* relative to current directory */
573
0
    const char *error;
574
0
    if (t_abspath(*binpath, binpath, &error) < 0) {
575
0
      *error_r = t_strdup_printf("t_abspath(%s) failed: %s",
576
0
               *binpath, error);
577
0
      return FALSE;
578
0
    }
579
0
    return TRUE;
580
0
  } else if ((path_env = getenv("PATH")) != NULL) {
581
    /* we have to find our executable from path */
582
0
    path = t_str_new(256);
583
0
    paths = t_strsplit(path_env, ":");
584
0
    for (; *paths != NULL; paths++) {
585
0
      str_append(path, *paths);
586
0
      str_append_c(path, '/');
587
0
      str_append(path, *binpath);
588
0
      if (access(str_c(path), X_OK) == 0) {
589
0
        *binpath = str_c(path);
590
0
        return TRUE;
591
0
      }
592
0
      str_truncate(path, 0);
593
0
    }
594
0
    *error_r = "Could not find the wanted executable from PATH";
595
0
    return FALSE;
596
0
  } else {
597
0
    *error_r = "PATH environment variable undefined";
598
0
    return FALSE;
599
0
  }
600
0
}