]> git.ipfire.org Git - thirdparty/dovecot/core.git/commitdiff
lib: Add Pigeonhole realpath functions to path-util
authorMartti Rannanjärvi <martti.rannanjarvi@dovecot.fi>
Sun, 4 Dec 2016 10:13:00 +0000 (12:13 +0200)
committerGitLab <gitlab@git.dovecot.net>
Mon, 30 Jan 2017 16:48:15 +0000 (18:48 +0200)
src/lib/path-util.c
src/lib/path-util.h

index 5a8dbbf63f497b14519af0be172bc17dc55155ed..b0f04b2c568e4ac39e15c6e792fecd24712a7b4b 100644 (file)
@@ -5,6 +5,270 @@
 #include "path-util.h"
 
 #include <unistd.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#define PATH_UTIL_MAX_PATH      8*1024
+#define PATH_UTIL_MAX_SYMLINKS  80
+
+static int t_getcwd_alloc(char **dir_r, size_t *asize_r,
+                         const char **error_r) ATTR_NULL(2)
+{
+       /* @UNSAFE */
+       char *dir;
+       size_t asize = 128;
+
+       dir = t_buffer_get(asize);
+       while (getcwd(dir, asize) == NULL) {
+               if (errno != ERANGE) {
+                       *error_r = t_strdup_printf("getcwd() failed: %m");
+                       return -1;
+               }
+               asize = nearest_power(asize+1);
+               dir = t_buffer_get(asize);
+       }
+       if (asize_r != NULL)
+               *asize_r = asize;
+       *dir_r = dir;
+       return 0;
+}
+
+static int path_normalize(const char *path, bool resolve_links,
+                         const char **npath_r, const char **error_r)
+{
+       /* @UNSAFE */
+       unsigned int link_count = 0;
+       char *npath, *npath_pos;
+       const char *p;
+       size_t asize;
+
+       i_assert(path != NULL);
+       i_assert(npath_r != NULL);
+       i_assert(error_r != NULL);
+
+       if (path[0] != '/') {
+               /* relative; initialize npath with current directory */
+               if (t_getcwd_alloc(&npath, &asize, error_r) < 0)
+                       return -1;
+               npath_pos = npath + strlen(npath);
+               i_assert(npath[0] == '/');
+       } else {
+               /* absolute; initialize npath with root */
+               asize = 128;
+               npath = t_buffer_get(asize);
+               npath[0] = '/';
+               npath_pos = npath + 1;
+       }
+
+       p = path;
+       while (*p != '\0') {
+               struct stat st;
+               ptrdiff_t seglen;
+               const char *segend;
+
+               /* skip duplicate shashes */
+               while (*p == '/')
+                       p++;
+
+               /* find end of path segment */
+               for (segend = p; *segend != '\0' && *segend != '/'; segend++);
+
+               if (segend == p)
+                       break; /* '\0' */
+               seglen = segend - p;
+               if (seglen == 1 && p[0] == '.') {
+                       /* a reference to this segment; nothing to do */
+               } else if (seglen == 2 && p[0] == '.' && p[1] == '.') {
+                       /* a reference to parent segment; back up to previous
+                        * slash */
+                       if (npath_pos > npath + 1) {
+                               if (*(npath_pos-1) == '/')
+                                       npath_pos--;
+                               for (; *(npath_pos-1) != '/'; npath_pos--);
+                       }
+               } else {
+                       /* make sure npath now ends in slash */
+                       if (*(npath_pos-1) != '/') {
+                               i_assert(npath_pos + 1 < npath + asize);
+                               *(npath_pos++) = '/';
+                       }
+
+                       /* allocate space if necessary */
+                       if ((npath_pos + seglen + 1) >= (npath + asize)) {
+                               ptrdiff_t npath_offset = npath_pos - npath;
+                               asize = nearest_power(npath_offset + seglen + 2);
+                               npath = t_buffer_reget(npath, asize);
+                               npath_pos = npath + npath_offset;
+                       }
+
+                       /* copy segment to normalized path */
+                       i_assert(p + seglen < npath + asize);
+                       (void)memmove(npath_pos, p, seglen);
+                       npath_pos += seglen;
+               }
+
+               if (resolve_links) {
+                       /* stat path up to here (segend points to tail) */
+                       *npath_pos = '\0';
+                       if (lstat(npath, &st) < 0) {
+                               *error_r = t_strdup_printf("lstat() failed: %m");
+                               return -1;
+                       }
+
+                       if (S_ISLNK (st.st_mode)) {
+                               /* symlink */
+                               char *npath_link;
+                               size_t lsize = 128, tlen = strlen(segend), espace;
+                               size_t ltlen = (link_count == 0 ? 0 : tlen);
+                               ssize_t ret;
+
+                               /* limit link dereferences */
+                               if (++link_count > PATH_UTIL_MAX_SYMLINKS) {
+                                       errno = ELOOP;
+                                       *error_r = "Too many symlink dereferences";
+                                       return -1;
+                               }
+
+                               /* allocate space for preserving tail of previous symlink and
+                                  first attempt at reading symlink with room for the tail
+
+                                  buffer will look like this:
+                                  [npath][0][preserved tail][link buffer][room for tail][0]
+                                */
+                               espace = ltlen + tlen + 2;
+                               if ((npath_pos + espace + lsize) >= (npath + asize)) {
+                                       ptrdiff_t npath_offset = npath_pos - npath;
+                                       asize = nearest_power((npath_offset + espace + lsize) + 1);
+                                       lsize = asize - (npath_offset + espace);
+                                       npath = t_buffer_reget(npath, asize);
+                                       npath_pos = npath + npath_offset;
+                               }
+
+                               if (ltlen > 0) {
+                                       /* preserve tail just after end of npath */
+                                       (void)memmove(npath_pos + 1, segend, ltlen);
+                               }
+
+                               /* read the symlink after the preserved tail */
+                               for (;;) {
+                                       npath_link = (npath_pos + 1) + ltlen;
+
+                                       i_assert(npath_link + lsize < npath + asize);
+
+                                       /* attempt to read the link */
+                                       if ((ret=readlink(npath, npath_link, lsize)) < 0) {
+                                               *error_r = t_strdup_printf("readlink() failed: %m");
+                                               return -1;
+                                       }
+                                       if ((size_t)ret < lsize) {
+                                               /* make static analyzers happy */
+                                               npath_link[ret] = '\0';
+                                               break;
+                                       }
+
+                                       /* sum of new symlink content length
+                                        * and path tail length may not
+                                          exceed maximum */
+                                       if ((size_t)(ret + tlen) >= PATH_UTIL_MAX_PATH) {
+                                               errno = ENAMETOOLONG;
+                                               *error_r = "Resulting path is too long";
+                                               return -1;
+                                       }
+
+                                       /* try again with bigger buffer */
+                                       espace = ltlen + tlen + 2;
+                                       if ((npath_pos + espace + lsize) >= (npath + asize)) {
+                                               ptrdiff_t npath_offset = npath_pos - npath;
+                                               asize = nearest_power((npath_offset + espace + lsize) + 1);
+                                               lsize = asize - (npath_offset + espace);
+                                               npath = t_buffer_reget(npath, asize);
+                                               npath_pos = npath + npath_offset;
+                                       }
+                               }
+
+                               /* add tail of previous path at end of symlink */
+                               if (ltlen > 0) {
+                                       i_assert(npath_pos + 1 + tlen < npath + asize);
+                                       (void)memcpy(npath_link + ret, npath_pos + 1, tlen);
+                               } else {
+                                       i_assert(segend + tlen < npath + asize);
+                                       (void)memcpy(npath_link + ret, segend, tlen);
+                               }
+                               *(npath_link+ret+tlen) = '\0';
+
+                               /* use as new source path */
+                               path = segend = npath_link;
+
+                               if (path[0] == '/') {
+                                       /* absolute symlink; start over at root */
+                                       npath_pos = npath + 1;
+                               } else {
+                                       /* relative symlink; back up to previous segment */
+                                       if (npath_pos > npath + 1) {
+                                               if (*(npath_pos-1) == '/')
+                                                       npath_pos--;
+                                               for (; *(npath_pos-1) != '/'; npath_pos--);
+                                       }
+                               }
+
+                       } else if (*segend != '\0' && !S_ISDIR (st.st_mode)) {
+                               /* not last segment, but not a directory either */
+                               errno = ENOTDIR;
+                               *error_r = t_strdup_printf("Not a directory: %s", npath);
+                               return -1;
+                       }
+               }
+
+               p = segend;
+       }
+
+       i_assert(npath_pos < npath + asize);
+
+       /* remove any trailing slash */
+       if (npath_pos > npath + 1 && *(npath_pos-1) == '/')
+               npath_pos--;
+       *npath_pos = '\0';
+
+       t_buffer_alloc(npath_pos - npath + 1);
+       *npath_r = npath;
+       return 0;
+}
+
+int t_normpath(const char *path, const char **npath_r, const char **error_r)
+{
+       return path_normalize(path, FALSE, npath_r, error_r);
+}
+
+int t_normpath_to(const char *path, const char *root, const char **npath_r,
+                 const char **error_r)
+{
+       i_assert(path != NULL);
+       i_assert(root != NULL);
+       i_assert(npath_r != NULL);
+
+       if (*path == '/')
+               return t_normpath(path, npath_r, error_r);
+
+       return t_normpath(t_strconcat(root, "/", path, NULL), npath_r, error_r);
+}
+
+int t_realpath(const char *path, const char **npath_r, const char **error_r)
+{
+       return path_normalize(path, TRUE, npath_r, error_r);
+}
+
+int t_realpath_to(const char *path, const char *root, const char **npath_r,
+                 const char **error_r)
+{
+       i_assert(path != NULL);
+       i_assert(root != NULL);
+       i_assert(npath_r != NULL);
+
+       if (*path == '/')
+               return t_realpath(path, npath_r, error_r);
+
+       return t_realpath(t_strconcat(root, "/", path, NULL), npath_r, error_r);
+}
 
 const char *t_abspath(const char *path)
 {
@@ -32,24 +296,9 @@ const char *t_abspath_to(const char *path, const char *root)
 
 int t_get_working_dir(const char **dir_r, const char **error_r)
 {
+       i_assert(dir_r != NULL);
        i_assert(error_r != NULL);
-
-       /* @UNSAFE */
-       char *dir;
-       size_t size = 128;
-
-       dir = t_buffer_get(size);
-       while (getcwd(dir, size) == NULL) {
-               if (errno != ERANGE) {
-                       *error_r = t_strdup_printf("getcwd() failed: %m");
-                       return -1;
-               }
-               size = nearest_power(size+1);
-               dir = t_buffer_get(size);
-       }
-       t_buffer_alloc(strlen(dir) + 1);
-       *dir_r = dir;
-       return 0;
+       return t_getcwd_alloc((char**)dir_r, NULL, error_r);
 }
 
 int t_readlink(const char *path, const char **dest_r, const char **error_r)
index 68c98acfb41c5e6ac705e048eef75d606a21fc79..0ac0a07e00d663366127a0c2313beb807d0844c1 100644 (file)
@@ -1,6 +1,40 @@
 #ifndef PATH_UTIL_H
 #define PATH_UTIL_H
 
+/* Returns path as the normalized absolute path, which means that './'
+ * and '../' components are resolved, and that duplicate and trailing
+ * slashes are removed. If it's not already the absolute path, it's
+ * assumed to be relative to the current working directory.
+ *
+ * NOTE: Be careful with this function. The resolution of '../' components
+ * with the parent component as if it were a normal directory is not valid
+ * if the path contains symbolic links.
+ *
+ * Returns 0 on success, and -1 on failure. errno and error_r are set on
+ * failure, and error_r cannot be NULL.
+ */
+int t_normpath(const char *path, const char **npath_r, const char **error_r);
+/* Like t_normpath(), but path is relative to given root. */
+int t_normpath_to(const char *path, const char *root, const char **npath_r,
+                 const char **error_r);
+
+/* Returns path as the real normalized absolute path, which means that all
+ * symbolic links in the path are resolved, that './' and '../' components
+ * are resolved, and that duplicate and trailing slashes are removed. If it's
+ * not already the absolute path, it's assumed to be relative to the current
+ * working directory.
+ *
+ * NOTE: This function calls stat() for each path component and more when
+ * there are symbolic links (just like POSIX realpath()).
+ *
+ * Returns 0 on success, and -1 on failure. errno and error_r are set on
+ * failure, and error_r cannot be NULL.
+ */
+int t_realpath(const char *path, const char **npath_r, const char **error_r);
+/* Like t_realpath(), but path is relative to given root. */
+int t_realpath_to(const char *path, const char *root, const char **npath_r,
+                 const char **error_r);
+
 /* Returns path as absolute path. If it's not already absolute path,
  * it's assumed to be relative to current working directory.
  *