/src/dovecot/src/lib/nfs-workarounds.c
Line | Count | Source (jump to first uncovered line) |
1 | | /* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */ |
2 | | |
3 | | /* |
4 | | These tests were done with various Linux 2.6 kernels, FreeBSD 6.2 and |
5 | | Solaris 8 and 10. |
6 | | |
7 | | Attribute cache is usually flushed with chown()ing or fchown()ing the file. |
8 | | The safest way would be to use uid=-1 gid=-1, but this doesn't work with |
9 | | Linux (it does with FreeBSD 6.2 and Solaris). So we'll first get the |
10 | | file's owner and use it. As long as we're not root the file's owner can't |
11 | | change accidentally. If would be possible to also use chmod()/fchmod(), but |
12 | | that's riskier since it could actually cause an unwanted change. |
13 | | |
14 | | Write cache can be flushed with fdatasync(). It's all we need, but other |
15 | | tested alternatives are: fcntl locking (Linux 2.6, Solaris), |
16 | | fchown() (Solaris) and dup()+close() (Linux 2.6, Solaris). |
17 | | |
18 | | Read cache flushing is more problematic. There's no universal way to do it. |
19 | | The working methods are: |
20 | | |
21 | | Linux 2.6: fcntl(), O_DIRECT |
22 | | Solaris: fchown(), fcntl(), dup()+close() |
23 | | FreeBSD 6.2: fchown() |
24 | | |
25 | | fchown() can be easily used for Solaris and FreeBSD, but Linux requires |
26 | | playing with locks. O_DIRECT requires CONFIG_NFS_DIRECTIO to be enabled, so |
27 | | we can't always use it. |
28 | | */ |
29 | | |
30 | | #include "lib.h" |
31 | | #include "path-util.h" |
32 | | #include "nfs-workarounds.h" |
33 | | |
34 | | #include <fcntl.h> |
35 | | #include <unistd.h> |
36 | | #include <sys/stat.h> |
37 | | |
38 | | #if defined (__linux__) || defined(__sun) |
39 | | # define READ_CACHE_FLUSH_FCNTL |
40 | | #endif |
41 | | #if defined(__FreeBSD__) || defined(__sun) |
42 | | # define ATTRCACHE_FLUSH_CHOWN_UID_1 |
43 | | #endif |
44 | | |
45 | | static void nfs_flush_file_handle_cache_parent_dir(const char *path); |
46 | | |
47 | | static int |
48 | | nfs_safe_do(const char *path, int (*callback)(const char *path, void *context), |
49 | | void *context) |
50 | 0 | { |
51 | 0 | unsigned int i; |
52 | 0 | int ret; |
53 | |
|
54 | 0 | for (i = 1;; i++) { |
55 | 0 | ret = callback(path, context); |
56 | 0 | if (ret == 0 || errno != ESTALE || i == NFS_ESTALE_RETRY_COUNT) |
57 | 0 | break; |
58 | | |
59 | | /* ESTALE: Some operating systems may fail with this if they |
60 | | can't internally revalidate the NFS file handle. Flush the |
61 | | file handle and try again */ |
62 | 0 | nfs_flush_file_handle_cache(path); |
63 | 0 | } |
64 | 0 | return ret; |
65 | 0 | } |
66 | | |
67 | | struct nfs_safe_open_context { |
68 | | int flags; |
69 | | int fd; |
70 | | }; |
71 | | |
72 | | static int nfs_safe_open_callback(const char *path, void *context) |
73 | 0 | { |
74 | 0 | struct nfs_safe_open_context *ctx = context; |
75 | |
|
76 | 0 | ctx->fd = open(path, ctx->flags); |
77 | 0 | return ctx->fd == -1 ? -1 : 0; |
78 | 0 | } |
79 | | |
80 | | int nfs_safe_open(const char *path, int flags) |
81 | 0 | { |
82 | 0 | struct nfs_safe_open_context ctx; |
83 | |
|
84 | 0 | i_assert((flags & O_CREAT) == 0); |
85 | | |
86 | 0 | ctx.flags = flags; |
87 | 0 | if (nfs_safe_do(path, nfs_safe_open_callback, &ctx) < 0) |
88 | 0 | return -1; |
89 | | |
90 | 0 | return ctx.fd; |
91 | 0 | } |
92 | | |
93 | | static int nfs_safe_stat_callback(const char *path, void *context) |
94 | 0 | { |
95 | 0 | struct stat *buf = context; |
96 | |
|
97 | 0 | return stat(path, buf); |
98 | 0 | } |
99 | | |
100 | | int nfs_safe_stat(const char *path, struct stat *buf) |
101 | 0 | { |
102 | 0 | return nfs_safe_do(path, nfs_safe_stat_callback, buf); |
103 | 0 | } |
104 | | |
105 | | static int nfs_safe_lstat_callback(const char *path, void *context) |
106 | 0 | { |
107 | 0 | struct stat *buf = context; |
108 | |
|
109 | 0 | return lstat(path, buf); |
110 | 0 | } |
111 | | |
112 | | int nfs_safe_lstat(const char *path, struct stat *buf) |
113 | 0 | { |
114 | 0 | return nfs_safe_do(path, nfs_safe_lstat_callback, buf); |
115 | 0 | } |
116 | | |
117 | | int nfs_safe_link(const char *oldpath, const char *newpath, bool links1) |
118 | 0 | { |
119 | 0 | struct stat st; |
120 | 0 | nlink_t orig_link_count = 1; |
121 | |
|
122 | 0 | if (!links1) { |
123 | 0 | if (stat(oldpath, &st) < 0) |
124 | 0 | return -1; |
125 | 0 | orig_link_count = st.st_nlink; |
126 | 0 | } |
127 | | |
128 | 0 | if (link(oldpath, newpath) == 0) { |
129 | 0 | #ifndef __FreeBSD__ |
130 | 0 | return 0; |
131 | 0 | #endif |
132 | | /* FreeBSD at least up to v6.2 converts EEXIST errors to |
133 | | success. */ |
134 | 0 | } else if (errno != EEXIST) |
135 | 0 | return -1; |
136 | | |
137 | | /* We don't know if it succeeded or failed. stat() to make sure. */ |
138 | 0 | if (stat(oldpath, &st) < 0) |
139 | 0 | return -1; |
140 | 0 | if (st.st_nlink == orig_link_count) { |
141 | 0 | errno = EEXIST; |
142 | 0 | return -1; |
143 | 0 | } |
144 | 0 | return 0; |
145 | 0 | } |
146 | | |
147 | | static void nfs_flush_chown_uid(const char *path) |
148 | 0 | { |
149 | |
|
150 | | #ifdef ATTRCACHE_FLUSH_CHOWN_UID_1 |
151 | | uid_t uid = (uid_t)-1; |
152 | | if (chown(path, uid, (gid_t)-1) < 0) { |
153 | | if (errno == ESTALE || errno == EPERM || errno == ENOENT) { |
154 | | /* attr cache is flushed */ |
155 | | return; |
156 | | } |
157 | | if (likely(errno == ENOENT)) { |
158 | | nfs_flush_file_handle_cache_parent_dir(path); |
159 | | return; |
160 | | } |
161 | | i_error("nfs_flush_chown_uid: chown(%s) failed: %m", path); |
162 | | } |
163 | | #else |
164 | 0 | struct stat st; |
165 | |
|
166 | 0 | if (stat(path, &st) == 0) { |
167 | | /* do nothing */ |
168 | 0 | } else { |
169 | 0 | if (errno == ESTALE) { |
170 | | /* ESTALE causes the OS to flush the attr cache */ |
171 | 0 | return; |
172 | 0 | } |
173 | 0 | if (likely(errno == ENOENT)) { |
174 | 0 | nfs_flush_file_handle_cache_parent_dir(path); |
175 | 0 | return; |
176 | 0 | } |
177 | 0 | i_error("nfs_flush_chown_uid: stat(%s) failed: %m", path); |
178 | 0 | return; |
179 | 0 | } |
180 | | /* we use chmod for this operation since chown has been seen to drop S_UID |
181 | | and S_GID bits from directory inodes in certain conditions */ |
182 | 0 | if (chmod(path, st.st_mode & 07777) < 0) { |
183 | 0 | if (errno == EPERM) { |
184 | | /* attr cache is flushed */ |
185 | 0 | return; |
186 | 0 | } |
187 | 0 | if (likely(errno == ENOENT)) { |
188 | 0 | nfs_flush_file_handle_cache_parent_dir(path); |
189 | 0 | return; |
190 | 0 | } |
191 | 0 | i_error("nfs_flush_chown_uid: chmod(%s, %04o) failed: %m", |
192 | 0 | path, st.st_mode & 07777); |
193 | 0 | } |
194 | 0 | #endif |
195 | 0 | } |
196 | | |
197 | | #ifdef __FreeBSD__ |
198 | | static bool nfs_flush_fchown_uid(const char *path, int fd) |
199 | | { |
200 | | uid_t uid; |
201 | | #ifndef ATTRCACHE_FLUSH_CHOWN_UID_1 |
202 | | struct stat st; |
203 | | |
204 | | if (fstat(fd, &st) < 0) { |
205 | | if (likely(errno == ESTALE)) |
206 | | return FALSE; |
207 | | i_error("nfs_flush_attr_cache_fchown: fstat(%s) failed: %m", |
208 | | path); |
209 | | return TRUE; |
210 | | } |
211 | | uid = st.st_uid; |
212 | | #else |
213 | | uid = (uid_t)-1; |
214 | | #endif |
215 | | if (fchown(fd, uid, (gid_t)-1) < 0) { |
216 | | if (errno == ESTALE) |
217 | | return FALSE; |
218 | | if (likely(errno == EACCES || errno == EPERM)) { |
219 | | /* attr cache is flushed */ |
220 | | return TRUE; |
221 | | } |
222 | | |
223 | | i_error("nfs_flush_attr_cache_fd_locked: fchown(%s) failed: %m", |
224 | | path); |
225 | | } |
226 | | return TRUE; |
227 | | } |
228 | | #endif |
229 | | |
230 | | #ifdef READ_CACHE_FLUSH_FCNTL |
231 | | static bool nfs_flush_fcntl(const char *path, int fd) |
232 | 0 | { |
233 | 0 | static bool locks_disabled = FALSE; |
234 | 0 | struct flock fl; |
235 | 0 | int ret; |
236 | |
|
237 | 0 | if (locks_disabled) |
238 | 0 | return FALSE; |
239 | | |
240 | | /* If the file was already locked, we'll just get the same lock |
241 | | again. It should succeed just fine. If was was unlocked, we'll |
242 | | have to get a lock and then unlock it. Linux 2.6 flushes read cache |
243 | | only when read/write locking succeeded. */ |
244 | 0 | fl.l_type = F_RDLCK; |
245 | 0 | fl.l_whence = SEEK_SET; |
246 | 0 | fl.l_start = 0; |
247 | 0 | fl.l_len = 0; |
248 | |
|
249 | 0 | alarm(60); |
250 | 0 | ret = fcntl(fd, F_SETLKW, &fl); |
251 | 0 | alarm(0); |
252 | |
|
253 | 0 | if (unlikely(ret < 0)) { |
254 | 0 | if (errno == ENOLCK) { |
255 | 0 | locks_disabled = TRUE; |
256 | 0 | return FALSE; |
257 | 0 | } |
258 | 0 | i_error("nfs_flush_fcntl: fcntl(%s, F_RDLCK) failed: %m", path); |
259 | 0 | return FALSE; |
260 | 0 | } |
261 | | |
262 | 0 | fl.l_type = F_UNLCK; |
263 | 0 | (void)fcntl(fd, F_SETLKW, &fl); |
264 | 0 | return TRUE; |
265 | 0 | } |
266 | | #endif |
267 | | |
268 | | void nfs_flush_attr_cache_unlocked(const char *path) |
269 | 0 | { |
270 | 0 | int fd; |
271 | | |
272 | | /* Try to flush the attribute cache the nice way first. */ |
273 | 0 | fd = open(path, O_RDONLY); |
274 | 0 | if (fd != -1) |
275 | 0 | i_close_fd(&fd); |
276 | 0 | else if (errno == ESTALE) { |
277 | | /* this already flushed the cache */ |
278 | 0 | } else { |
279 | | /* most likely ENOENT, which means a negative cache hit. |
280 | | flush the file handles for its parent directory. */ |
281 | 0 | nfs_flush_file_handle_cache_parent_dir(path); |
282 | 0 | } |
283 | 0 | } |
284 | | |
285 | | void nfs_flush_attr_cache_maybe_locked(const char *path) |
286 | 0 | { |
287 | 0 | nfs_flush_chown_uid(path); |
288 | 0 | } |
289 | | |
290 | | void nfs_flush_attr_cache_fd_locked(const char *path ATTR_UNUSED, |
291 | | int fd ATTR_UNUSED) |
292 | 0 | { |
293 | | #ifdef __FreeBSD__ |
294 | | /* FreeBSD doesn't flush attribute cache with fcntl(), so we have |
295 | | to do it ourself. */ |
296 | | (void)nfs_flush_fchown_uid(path, fd); |
297 | | #else |
298 | | /* Linux and Solaris are fine. */ |
299 | 0 | #endif |
300 | 0 | } |
301 | | |
302 | | static bool |
303 | | nfs_flush_file_handle_cache_dir(const char *path, bool try_parent ATTR_UNUSED) |
304 | 0 | { |
305 | 0 | #ifdef __linux__ |
306 | | /* chown()ing parent is the safest way to handle this */ |
307 | 0 | nfs_flush_chown_uid(path); |
308 | | #else |
309 | | /* rmdir() is the only choice with FreeBSD and Solaris */ |
310 | | if (unlikely(rmdir(path) == 0)) { |
311 | | if (mkdir(path, 0700) == 0) { |
312 | | i_warning("nfs_flush_file_handle_cache_dir: " |
313 | | "rmdir(%s) unexpectedly " |
314 | | "removed the dir. recreated.", path); |
315 | | } else { |
316 | | i_warning("nfs_flush_file_handle_cache_dir: " |
317 | | "rmdir(%s) unexpectedly " |
318 | | "removed the dir. mkdir() failed: %m", path); |
319 | | } |
320 | | } else if (errno == ESTALE || errno == ENOTDIR || |
321 | | errno == ENOTEMPTY || errno == EEXIST || errno == EACCES) { |
322 | | /* expected failures */ |
323 | | } else if (errno == ENOENT) { |
324 | | return FALSE; |
325 | | } else if (errno == EINVAL && try_parent) { |
326 | | /* Solaris gives this if we're trying to rmdir() the current |
327 | | directory. Work around this by temporarily changing the |
328 | | current directory to the parent directory. */ |
329 | | const char *cur_path, *p; |
330 | | int cur_dir_fd; |
331 | | bool ret; |
332 | | |
333 | | cur_dir_fd = open(".", O_RDONLY); |
334 | | if (cur_dir_fd == -1) { |
335 | | i_error("open(.) failed for: %m"); |
336 | | return TRUE; |
337 | | } |
338 | | |
339 | | const char *error; |
340 | | if (t_get_working_dir(&cur_path, &error) < 0) { |
341 | | i_error("nfs_flush_file_handle_cache_dir: %s", error); |
342 | | i_close_fd(&cur_dir_fd); |
343 | | return TRUE; |
344 | | } |
345 | | p = strrchr(cur_path, '/'); |
346 | | if (p == NULL) |
347 | | cur_path = "/"; |
348 | | else |
349 | | cur_path = t_strdup_until(cur_path, p); |
350 | | if (chdir(cur_path) < 0) { |
351 | | i_error("nfs_flush_file_handle_cache_dir: " |
352 | | "chdir() failed"); |
353 | | } |
354 | | ret = nfs_flush_file_handle_cache_dir(path, FALSE); |
355 | | if (fchdir(cur_dir_fd) < 0) |
356 | | i_error("fchdir() failed: %m"); |
357 | | i_close_fd(&cur_dir_fd); |
358 | | return ret; |
359 | | } else { |
360 | | i_error("nfs_flush_file_handle_cache_dir: " |
361 | | "rmdir(%s) failed: %m", path); |
362 | | } |
363 | | #endif |
364 | 0 | return TRUE; |
365 | 0 | } |
366 | | |
367 | | static void nfs_flush_file_handle_cache_parent_dir(const char *path) |
368 | 0 | { |
369 | 0 | const char *p; |
370 | |
|
371 | 0 | p = strrchr(path, '/'); |
372 | 0 | T_BEGIN { |
373 | 0 | if (p == NULL) |
374 | 0 | (void)nfs_flush_file_handle_cache_dir(".", TRUE); |
375 | 0 | else |
376 | 0 | (void)nfs_flush_file_handle_cache_dir(t_strdup_until(path, p), |
377 | 0 | TRUE); |
378 | 0 | } T_END; |
379 | 0 | } |
380 | | |
381 | | void nfs_flush_file_handle_cache(const char *path) |
382 | 0 | { |
383 | 0 | nfs_flush_file_handle_cache_parent_dir(path); |
384 | 0 | } |
385 | | |
386 | | void nfs_flush_read_cache_locked(const char *path ATTR_UNUSED, |
387 | | int fd ATTR_UNUSED) |
388 | 0 | { |
389 | 0 | #ifdef READ_CACHE_FLUSH_FCNTL |
390 | | /* already flushed when fcntl() was called */ |
391 | | #else |
392 | | /* we can only hope that underlying filesystem uses micro/nanosecond |
393 | | resolution so that attribute cache flushing notices mtime changes */ |
394 | | nfs_flush_attr_cache_fd_locked(path, fd); |
395 | | #endif |
396 | 0 | } |
397 | | |
398 | | void nfs_flush_read_cache_unlocked(const char *path, int fd) |
399 | 0 | { |
400 | 0 | #ifdef READ_CACHE_FLUSH_FCNTL |
401 | 0 | if (!nfs_flush_fcntl(path, fd)) |
402 | 0 | nfs_flush_attr_cache_fd_locked(path, fd); |
403 | | #else |
404 | | nfs_flush_read_cache_locked(path, fd); |
405 | | #endif |
406 | 0 | } |