Coverage Report

Created: 2023-11-19 07:06

/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
}