1 /*******************************************************************************
2   Copyright (c) 2014 Vladimir Kondratyev <vladimir@kondratyev.su>
3   SPDX-License-Identifier: MIT
4 
5   Permission is hereby granted, free of charge, to any person obtaining a copy
6   of this software and associated documentation files (the "Software"), to deal
7   in the Software without restriction, including without limitation the rights
8   to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9   copies of the Software, and to permit persons to whom the Software is
10   furnished to do so, subject to the following conditions:
11 
12   The above copyright notice and this permission notice shall be included in
13   all copies or substantial portions of the Software.
14 
15   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16   IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17   FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18   AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19   LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20   OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21   THE SOFTWARE.
22 *******************************************************************************/
23 
24 #include <sys/param.h> /* MAXPATHLEN */
25 #include <sys/types.h>
26 #include <sys/stat.h>  /* stat */
27 
28 #include <assert.h>
29 #include <dirent.h> /* opendir */
30 #include <errno.h>  /* errno */
31 #include <fcntl.h>  /* fcntl */
32 #include <limits.h> /* PATH_MAX */
33 #include <stdlib.h> /* malloc */
34 #include <string.h> /* memset */
35 #include <unistd.h> /* fchdir */
36 
37 #include "compat.h"
38 #include "config.h"
39 
40 typedef struct dirpath_t {
41     ino_t inode;              /* inode number */
42     dev_t dev;                /* device number */
43     char *path;               /* full path to inode */
44     RB_ENTRY(dirpath_t) link; /* RB tree links */
45 } dirpath_t;
46 
47 /* directory path cache */
48 static RB_HEAD(dp, dirpath_t) dirs = RB_INITIALIZER (&dirs);
49 /* directory path cache mutex */
50 static pthread_mutex_t dirs_mtx = PTHREAD_MUTEX_INITIALIZER;
51 
52 /**
53  * Custom comparison function that can compare directory inode values
54  * through pointers passed by RB tree functions
55  *
56  * @param[in] dp1 A pointer to a first directory to compare
57  * @param[in] dp2 A pointer to a second directory to compare
58  * @return An -1, 0, or +1 if the first inode is considered to be respectively
59  *     less than, equal to, or greater than the second one.
60  **/
61 static int
dirpath_cmp(dirpath_t * dp1,dirpath_t * dp2)62 dirpath_cmp (dirpath_t *dp1, dirpath_t *dp2)
63 {
64     if (dp1->dev == dp2->dev)
65         return ((dp1->inode > dp2->inode) - (dp1->inode < dp2->inode));
66     else
67         return ((dp1->dev > dp2->dev) - (dp1->dev < dp2->dev));
68 }
69 
70 RB_GENERATE(dp, dirpath_t, link, dirpath_cmp);
71 
72 /**
73  * Returns a pointer to the absolute pathname of the directory by filedes
74  * NOTE: It uses unsafe fchdir if no F_GETPATH fcntl have been found
75  *
76  * @param[in] fd A file descriptor of opened directory
77  * @return a pointer to the pathname on success, NULL otherwise
78  **/
79 static char *
fd_getpath(int fd)80 fd_getpath (int fd)
81 {
82     assert (fd != -1);
83 
84     char *path = NULL;
85     struct stat st;
86 
87     if (fstat (fd, &st) == -1) {
88         return NULL;
89     }
90 
91     if (!S_ISDIR (st.st_mode)) {
92         errno = ENOTDIR;
93         return NULL;
94     }
95 
96     if (st.st_nlink == 0) {
97         errno = ENOENT;
98         return NULL;
99     }
100 
101 #if defined (F_GETPATH)
102     path = malloc (MAXPATHLEN);
103     if (path != NULL && fcntl (fd, F_GETPATH, path) == -1) {
104         free (path);
105         path = NULL;
106     }
107 #elif defined (ENABLE_UNSAFE_FCHDIR)
108     /*
109      * Using of this code path can be unsafe in multithreading applications as
110      * following code do a temporary change of global current working directory
111      * via fchdir call. Consider renaming of watched directory as relatively
112      * rare operation so catching such a race is unlikely
113      */
114     DIR *save = opendir(".");
115 
116     if (fchdir (fd) == 0) {
117         path = malloc (PATH_MAX);
118         if (path != NULL && getcwd (path, PATH_MAX) == NULL) {
119             free (path);
120             path = NULL;
121         }
122     }
123 
124     if (save != NULL) {
125         int saved_errno = errno;
126         fchdir (dirfd (save));
127         closedir (save);
128         errno = saved_errno;
129     }
130 #endif /* ENABLE_UNSAFE_FCHDIR && F_GETPATH */
131 
132     return path;
133 }
134 
135 /**
136  * Remove directory path from directory path cache.
137  * Should be called with dirs_mtx mutex held
138  *
139  * @param [in] dir A pointer to a directory to remove from cache
140  **/
141 static void
dir_remove(dirpath_t * dir)142 dir_remove (dirpath_t *dir)
143 {
144     if (dir->path != NULL) {
145         free (dir->path);
146     }
147     RB_REMOVE (dp, &dirs, dir);
148     free (dir);
149 }
150 
151 /**
152  * Insert directory path into directory path cache.
153  * Should be called with dirs_mtx mutex held
154  *
155  * @param [in] path  A directory path to be cached
156  * @param [in] inode A inode number of cached directory path
157  * @param [in] dev   A device number of cached directory path
158  * @return A pointer to allocated cache entry
159  **/
160 static dirpath_t *
dir_insert(const char * path,ino_t inode,dev_t dev)161 dir_insert (const char* path, ino_t inode, dev_t dev)
162 {
163     assert (path != NULL);
164 
165     dirpath_t *newdp, *olddp;
166 
167     newdp = calloc (1, sizeof (dirpath_t));
168     if (newdp == NULL) {
169         return NULL;
170     }
171 
172     newdp->inode = inode;
173     newdp->dev = dev;
174 
175     olddp = RB_FIND (dp, &dirs, newdp);
176     if (olddp != NULL) {
177         free (newdp);
178         if (strcmp (path, olddp->path)) {
179             free (olddp->path);
180             olddp->path = strdup (path);
181         }
182         newdp = olddp;
183     } else {
184         newdp->path = strdup (path);
185         RB_INSERT (dp, &dirs, newdp);
186     }
187 
188     if (newdp->path == NULL) {
189         dir_remove (newdp);
190         newdp = NULL;
191     }
192 
193     return newdp;
194 }
195 
196 /**
197  * Find cached directory path corresponding a given inode number
198  *
199  * @param[in] fd A file descriptor of opened directory
200  * @return a pointer to the pathname on success, NULL otherwise
201  **/
202 char *
fd_getpath_cached(int fd)203 fd_getpath_cached (int fd)
204 {
205     assert (fd != -1);
206 
207     dirpath_t find, *dir;
208     struct stat st1, st2;
209     char *path;
210 
211     if (fstat (fd, &st1) == -1) {
212         return NULL;
213     }
214 
215     if (!S_ISDIR (st1.st_mode)) {
216         errno = ENOTDIR;
217         return NULL;
218     }
219 
220     find.inode = st1.st_ino;
221     find.dev = st1.st_dev;
222 
223     pthread_mutex_lock (&dirs_mtx);
224 
225     dir = RB_FIND (dp, &dirs, &find);
226     if (dir == NULL || stat (dir->path, &st2) != 0
227       || dir->inode != st2.st_ino || dir->dev != st2.st_dev) {
228 
229         path = fd_getpath (fd);
230         if (path != NULL) {
231             dir = dir_insert (path, st1.st_ino, st1.st_dev);
232             free (path);
233         } else {
234             if (dir != NULL) {
235                 dir_remove (dir);
236                 dir = NULL;
237             }
238         }
239     }
240 
241     pthread_mutex_unlock (&dirs_mtx);
242 
243     return dir != NULL ? dir->path : NULL;
244 }
245 
246 /**
247  * Create a file path using its name and a path to its directory.
248  *
249  * @param[in] dir  A path to a file directory. May end with a '/'.
250  * @param[in] file File name.
251  * @return A concatenated path. Should be freed with free().
252  **/
253 static char*
path_concat(const char * dir,const char * file)254 path_concat (const char *dir, const char *file)
255 {
256     assert (dir != NULL);
257     assert (file != NULL);
258 
259     size_t dir_len, file_len, alloc_sz;
260     char *path;
261 
262     dir_len = strlen (dir);
263     file_len = strlen (file);
264     alloc_sz = dir_len + file_len + 2;
265 
266     path = malloc (alloc_sz);
267     if (path != NULL) {
268 
269         strlcpy (path, dir, alloc_sz);
270 
271         if (dir[dir_len - 1] != '/') {
272             ++dir_len;
273             path[dir_len - 1] = '/';
274         }
275 
276         strlcpy (path + dir_len, file, file_len + 1);
277     }
278 
279     return path;
280 }
281 
282 /**
283  * Create a file path using its name and a filedes of its directory.
284  *
285  * @param[in] fd   A file descriptor of opened directory.
286  * @param[in] file File name.
287  * @return A concatenated path. Should be freed with free().
288  **/
289 char*
fd_concat(int fd,const char * file)290 fd_concat (int fd, const char *file)
291 {
292     char *path = NULL;
293     struct stat st;
294 
295     if (fd == AT_FDCWD || file[0] == '/') {
296 
297         if (stat (file, &st) != -1
298           && S_ISDIR (st.st_mode)
299           && (path = malloc (PATH_MAX + 1)) != NULL
300           && realpath (file, path) == path) {
301 
302             pthread_mutex_lock (&dirs_mtx);
303             dir_insert (path, st.st_ino, st.st_dev);
304             pthread_mutex_unlock (&dirs_mtx);
305         } else {
306             if (path == NULL) {
307                 path = strdup (file);
308             } else {
309                 strlcpy (path, file, PATH_MAX + 1);
310             }
311         }
312     } else {
313 
314         char *dirpath = fd_getpath_cached (fd);
315         if (dirpath != NULL) {
316             path = path_concat (dirpath, file);
317         }
318     }
319 
320     return path;
321 }
322