Coverage Report

Created: 2026-01-10 07:11

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) 2009-2018 Dovecot authors, see the included COPYING file */
2
3
#include "lib.h"
4
#include "str.h"
5
#include "path-util.h"
6
7
#include <unistd.h>
8
#include <sys/types.h>
9
#include <sys/stat.h>
10
11
0
#define PATH_UTIL_MAX_PATH      8*1024
12
0
#define PATH_UTIL_MAX_SYMLINKS  80
13
14
static int t_getcwd_noalloc(char **dir_r, size_t *asize_r,
15
          const char **error_r) ATTR_NULL(2)
16
0
{
17
  /* @UNSAFE */
18
0
  char *dir;
19
0
  size_t asize = 128;
20
21
0
  dir = t_buffer_get(asize);
22
0
  while (getcwd(dir, asize) == NULL) {
23
0
    if (errno != ERANGE) {
24
0
      *error_r = t_strdup_printf("getcwd() failed: %m");
25
0
      return -1;
26
0
    }
27
0
    asize = nearest_power(asize+1);
28
0
    dir = t_buffer_get(asize);
29
0
  }
30
0
  if (asize_r != NULL)
31
0
    *asize_r = asize;
32
0
  *dir_r = dir;
33
0
  return 0;
34
0
}
35
36
static int path_normalize(const char *path, bool resolve_links,
37
        const char **npath_r, const char **error_r)
38
0
{
39
  /* @UNSAFE */
40
0
  unsigned int link_count = 0;
41
0
  char *npath, *npath_pos;
42
0
  const char *p;
43
0
  size_t asize;
44
45
0
  i_assert(path != NULL);
46
0
  i_assert(npath_r != NULL);
47
0
  i_assert(error_r != NULL);
48
49
0
  if (path[0] != '/') {
50
    /* relative; initialize npath with current directory */
51
0
    if (t_getcwd_noalloc(&npath, &asize, error_r) < 0)
52
0
      return -1;
53
0
    npath_pos = npath + strlen(npath);
54
0
    i_assert(npath[0] == '/');
55
0
  } else {
56
    /* absolute; initialize npath with root */
57
0
    asize = 128;
58
0
    npath = t_buffer_get(asize);
59
0
    npath[0] = '/';
60
0
    npath_pos = npath + 1;
61
0
  }
62
63
0
  p = path;
64
0
  while (*p != '\0') {
65
0
    struct stat st;
66
0
    ptrdiff_t seglen;
67
0
    const char *segend;
68
69
    /* skip duplicate slashes */
70
0
    while (*p == '/')
71
0
      p++;
72
73
    /* find end of path segment */
74
0
    for (segend = p; *segend != '\0' && *segend != '/'; segend++);
75
76
0
    if (segend == p)
77
0
      break; /* '\0' */
78
0
    seglen = segend - p;
79
0
    if (seglen == 1 && p[0] == '.') {
80
      /* a reference to this segment; nothing to do */
81
0
    } else if (seglen == 2 && p[0] == '.' && p[1] == '.') {
82
      /* a reference to parent segment; back up to previous
83
       * slash */
84
0
      i_assert(npath_pos >= npath);
85
0
      if ((npath_pos - npath) > 1) {
86
0
        if (*(npath_pos-1) == '/')
87
0
          npath_pos--;
88
0
        for (; *(npath_pos-1) != '/'; npath_pos--);
89
0
      }
90
0
    } else {
91
      /* allocate space if necessary */
92
0
      i_assert(npath_pos >= npath);
93
0
      if ((size_t)((npath_pos - npath) + seglen + 1) >= asize) {
94
0
        ptrdiff_t npath_offset = npath_pos - npath;
95
0
        asize = nearest_power(npath_offset + seglen + 2);
96
0
        npath = t_buffer_reget(npath, asize);
97
0
        npath_pos = npath + npath_offset;
98
0
      }
99
100
      /* make sure npath now ends in slash */
101
0
      i_assert(npath_pos > npath);
102
0
      if (*(npath_pos-1) != '/') {
103
0
        i_assert((size_t)((npath_pos - npath) + 1) < asize);
104
0
        *(npath_pos++) = '/';
105
0
      }
106
107
      /* copy segment to normalized path */
108
0
      i_assert(npath_pos >= npath);
109
0
      i_assert((size_t)((npath_pos - npath) + seglen) < asize);
110
0
      memmove(npath_pos, p, seglen);
111
0
      npath_pos += seglen;
112
0
    }
113
114
0
    if (resolve_links) {
115
      /* stat path up to here (segend points to tail) */
116
0
      *npath_pos = '\0';
117
0
      if (lstat(npath, &st) < 0) {
118
0
        *error_r = t_strdup_printf("lstat() failed: %m");
119
0
        return -1;
120
0
      }
121
122
0
      if (S_ISLNK (st.st_mode)) {
123
        /* symlink */
124
0
        char *npath_link;
125
0
        size_t lsize = 128, tlen = strlen(segend), espace;
126
0
        size_t ltlen = (link_count == 0 ? 0 : tlen);
127
0
        ssize_t ret;
128
129
        /* limit link dereferences */
130
0
        if (++link_count > PATH_UTIL_MAX_SYMLINKS) {
131
0
          errno = ELOOP;
132
0
          *error_r = "Too many symlink dereferences";
133
0
          return -1;
134
0
        }
135
136
        /* allocate space for preserving tail of previous symlink and
137
           first attempt at reading symlink with room for the tail
138
139
           buffer will look like this:
140
           [npath][0][preserved tail][link buffer][room for tail][0]
141
         */
142
0
        espace = ltlen + tlen + 2;
143
0
        i_assert(npath_pos >= npath);
144
0
        if ((size_t)((npath_pos - npath) + espace + lsize) >= asize) {
145
0
          ptrdiff_t npath_offset = npath_pos - npath;
146
0
          asize = nearest_power((npath_offset + espace + lsize) + 1);
147
0
          lsize = asize - (npath_offset + espace);
148
0
          npath = t_buffer_reget(npath, asize);
149
0
          npath_pos = npath + npath_offset;
150
0
        }
151
152
0
        if (ltlen > 0) {
153
          /* preserve tail just after end of npath */
154
0
          i_assert(npath_pos >= npath);
155
0
          i_assert((size_t)((npath_pos + 1 - npath) + ltlen) < asize);
156
0
          memmove(npath_pos + 1, segend, ltlen);
157
0
        }
158
159
        /* read the symlink after the preserved tail */
160
0
        for (;;) {
161
0
          npath_link = (npath_pos + 1) + ltlen;
162
163
0
          i_assert(npath_link >= npath_pos);
164
0
          i_assert((size_t)((npath_link - npath) + lsize) < asize);
165
166
          /* attempt to read the link */
167
0
          if ((ret=readlink(npath, npath_link, lsize)) < 0) {
168
0
            *error_r = t_strdup_printf("readlink() failed: %m");
169
0
            return -1;
170
0
          }
171
0
          if ((size_t)ret < lsize) {
172
            /* POSIX doesn't guarantee the presence of a NIL */
173
0
            npath_link[ret] = '\0';
174
0
            break;
175
0
          }
176
177
          /* sum of new symlink content length
178
           * and path tail length may not
179
             exceed maximum */
180
0
          if ((size_t)(ret + tlen) >= PATH_UTIL_MAX_PATH) {
181
0
            errno = ENAMETOOLONG;
182
0
            *error_r = "Resulting path is too long";
183
0
            return -1;
184
0
          }
185
186
          /* try again with bigger buffer,
187
             we need to allocate more space as well if lsize == ret,
188
             because the returned link may have gotten truncated */
189
0
          espace = ltlen + tlen + 2;
190
0
          i_assert(npath_pos >= npath);
191
0
          if ((size_t)((npath_pos - npath) + espace + lsize) >= asize ||
192
0
              lsize == (size_t)ret) {
193
0
            ptrdiff_t npath_offset = npath_pos - npath;
194
0
            asize = nearest_power((npath_offset + espace + lsize) + 1);
195
0
            lsize = asize - (npath_offset + espace);
196
0
            npath = t_buffer_reget(npath, asize);
197
0
            npath_pos = npath + npath_offset;
198
0
          }
199
0
        }
200
201
        /* add tail of previous path at end of symlink */
202
0
        i_assert(npath_link >= npath);
203
0
        if (ltlen > 0) {
204
0
          i_assert(npath_pos >= npath);
205
0
          i_assert((size_t)((npath_pos - npath) + 1 + tlen) < asize);
206
0
          i_assert((size_t)((npath_link - npath) + ret + tlen) < asize);
207
0
          memcpy(npath_link + ret, npath_pos + 1, tlen);
208
0
        } else {
209
0
          i_assert((size_t)((npath_link - npath) + ret + tlen) < asize);
210
0
          memcpy(npath_link + ret, segend, tlen);
211
0
        }
212
0
        *(npath_link+ret+tlen) = '\0';
213
214
        /* use as new source path */
215
0
        path = segend = npath_link;
216
217
0
        if (path[0] == '/') {
218
          /* absolute symlink; start over at root */
219
0
          npath_pos = npath + 1;
220
0
        } else {
221
          /* relative symlink; back up to previous segment */
222
0
          i_assert(npath_pos >= npath);
223
0
          if ((npath_pos - npath) > 1) {
224
0
            if (*(npath_pos-1) == '/')
225
0
              npath_pos--;
226
0
            for (; *(npath_pos-1) != '/'; npath_pos--);
227
0
          }
228
0
        }
229
230
0
      } else if (*segend != '\0' && !S_ISDIR (st.st_mode)) {
231
        /* not last segment, but not a directory either */
232
0
        errno = ENOTDIR;
233
0
        *error_r = t_strdup_printf("Not a directory: %s", npath);
234
0
        return -1;
235
0
      }
236
0
    }
237
238
0
    p = segend;
239
0
  }
240
241
0
  i_assert(npath_pos >= npath);
242
0
  i_assert((size_t)(npath_pos - npath) < asize);
243
244
  /* remove any trailing slash */
245
0
  if ((npath_pos - npath) > 1 && *(npath_pos-1) == '/')
246
0
    npath_pos--;
247
0
  *npath_pos = '\0';
248
249
0
  t_buffer_alloc(npath_pos - npath + 1);
250
0
  *npath_r = npath;
251
0
  return 0;
252
0
}
253
254
int t_normpath(const char *path, const char **npath_r, const char **error_r)
255
0
{
256
0
  return path_normalize(path, FALSE, npath_r, error_r);
257
0
}
258
259
int t_normpath_to(const char *path, const char *root, const char **npath_r,
260
      const char **error_r)
261
0
{
262
0
  i_assert(path != NULL);
263
0
  i_assert(root != NULL);
264
0
  i_assert(npath_r != NULL);
265
266
0
  if (*path == '/')
267
0
    return t_normpath(path, npath_r, error_r);
268
269
0
  return t_normpath(t_strconcat(root, "/", path, NULL), npath_r, error_r);
270
0
}
271
272
int t_realpath(const char *path, const char **npath_r, const char **error_r)
273
0
{
274
0
  return path_normalize(path, TRUE, npath_r, error_r);
275
0
}
276
277
int t_realpath_to(const char *path, const char *root, const char **npath_r,
278
      const char **error_r)
279
0
{
280
0
  i_assert(path != NULL);
281
0
  i_assert(root != NULL);
282
0
  i_assert(npath_r != NULL);
283
284
0
  if (*path == '/')
285
0
    return t_realpath(path, npath_r, error_r);
286
287
0
  return t_realpath(t_strconcat(root, "/", path, NULL), npath_r, error_r);
288
0
}
289
290
int t_abspath(const char *path, const char **abspath_r, const char **error_r)
291
0
{
292
0
  i_assert(path != NULL);
293
0
  i_assert(abspath_r != NULL);
294
0
  i_assert(error_r != NULL);
295
296
0
  if (*path == '/') {
297
0
    *abspath_r = path;
298
0
    return 0;
299
0
  }
300
301
0
  const char *dir, *error;
302
0
  if (t_get_working_dir(&dir, &error) < 0) {
303
0
    *error_r = t_strconcat("Failed to get working directory: ",
304
0
               error, NULL);
305
0
    return -1;
306
0
  }
307
0
  *abspath_r = t_strconcat(dir, "/", path, NULL);
308
0
  return 0;
309
0
}
310
311
const char *t_abspath_to(const char *path, const char *root)
312
0
{
313
0
  i_assert(path != NULL);
314
0
  i_assert(root != NULL);
315
316
0
  if (*path == '/')
317
0
    return path;
318
319
0
  return t_strconcat(root, "/", path, NULL);
320
0
}
321
322
int t_get_working_dir(const char **dir_r, const char **error_r)
323
0
{
324
0
  char *dir;
325
326
0
  i_assert(dir_r != NULL);
327
0
  i_assert(error_r != NULL);
328
0
  if (t_getcwd_noalloc(&dir, NULL, error_r) < 0)
329
0
    return -1;
330
331
0
  t_buffer_alloc(strlen(dir) + 1);
332
0
  *dir_r = dir;
333
0
  return 0;
334
0
}
335
336
int t_readlink(const char *path, const char **dest_r, const char **error_r)
337
0
{
338
0
  i_assert(error_r != NULL);
339
340
  /* @UNSAFE */
341
0
  ssize_t ret;
342
0
  char *dest;
343
0
  size_t size = 128;
344
345
0
  dest = t_buffer_get(size);
346
0
  while ((ret = readlink(path, dest, size)) >= (ssize_t)size) {
347
0
    size = nearest_power(size+1);
348
0
    dest = t_buffer_get(size);
349
0
  }
350
0
  if (ret < 0) {
351
0
    *error_r = t_strdup_printf("readlink() failed: %m");
352
0
    return -1;
353
0
  }
354
355
0
  dest[ret] = '\0';
356
0
  t_buffer_alloc(ret + 1);
357
0
  *dest_r = dest;
358
0
  return 0;
359
0
}
360
361
bool t_binary_abspath(const char **binpath, const char **error_r)
362
0
{
363
0
  const char *path_env, *const *paths;
364
0
  string_t *path;
365
366
0
  if (**binpath == '/') {
367
    /* already have absolute path */
368
0
    return TRUE;
369
0
  } else if (strchr(*binpath, '/') != NULL) {
370
    /* relative to current directory */
371
0
    const char *error;
372
0
    if (t_abspath(*binpath, binpath, &error) < 0) {
373
0
      *error_r = t_strdup_printf("t_abspath(%s) failed: %s",
374
0
               *binpath, error);
375
0
      return FALSE;
376
0
    }
377
0
    return TRUE;
378
0
  } else if ((path_env = getenv("PATH")) != NULL) {
379
    /* we have to find our executable from path */
380
0
    path = t_str_new(256);
381
0
    paths = t_strsplit(path_env, ":");
382
0
    for (; *paths != NULL; paths++) {
383
0
      str_append(path, *paths);
384
0
      str_append_c(path, '/');
385
0
      str_append(path, *binpath);
386
0
      if (access(str_c(path), X_OK) == 0) {
387
0
        *binpath = str_c(path);
388
0
        return TRUE;
389
0
      }
390
0
      str_truncate(path, 0);
391
0
    }
392
0
    *error_r = "Could not find the wanted executable from PATH";
393
0
    return FALSE;
394
0
  } else {
395
0
    *error_r = "PATH environment variable undefined";
396
0
    return FALSE;
397
0
  }
398
0
}