From 3368d0bb01a0bcf08222382600eba39ffac156bf Mon Sep 17 00:00:00 2001 From: Ben Noordhuis Date: Mon, 8 Nov 2021 11:39:07 +0100 Subject: [PATCH] linux: work around CIFS EPERM bug It's been reported that copy_file_range() on a CIFS mount fails with an EPERM error, even though the source and target files have the appropriate permissions. This is probably a variation on the EPERM error that libuv handles when it tries to fchmod() a file on a CIFS mount that hasn't been mounted with the "noperm" option, so let's handle it here too. This commit applies minor refactoring because the "work around file system bugs" section got quite unwieldy. I also snuck in a bug fix where the workaround for buggy CephFS mounts disabled copy_file_range() permanently. Fixes: https://github.com/libuv/libuv/issues/3322 --- src/unix/fs.c | 119 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 79 insertions(+), 40 deletions(-) diff --git a/src/unix/fs.c b/src/unix/fs.c index eb17fb4a23e..362c36c6a0c 100644 --- a/src/unix/fs.c +++ b/src/unix/fs.c @@ -946,6 +946,71 @@ static int uv__is_buggy_cephfs(int fd) { return uv__kernel_version() < /* 4.20.0 */ 0x041400; } + + +static int uv__is_cifs_or_smb(int fd) { + struct statfs s; + + if (-1 == fstatfs(fd, &s)) + return 0; + + switch ((unsigned) s.f_type) { + case 0x0000517Bu: /* SMB */ + case 0xFE534D42u: /* SMB2 */ + case 0xFF534D42u: /* CIFS */ + return 1; + } + + return 0; +} + + +static ssize_t uv__fs_try_copy_file_range(int in_fd, off_t* off, + int out_fd, size_t len) { + static int no_copy_file_range_support; + ssize_t r; + + if (uv__load_relaxed(&no_copy_file_range_support)) { + errno = ENOSYS; + return -1; + } + + r = uv__fs_copy_file_range(in_fd, off, out_fd, NULL, len, 0); + + if (r != -1) + return r; + + switch (errno) { + case EACCES: + /* Pre-4.20 kernels have a bug where CephFS uses the RADOS + * copy-from command when it shouldn't. + */ + if (uv__is_buggy_cephfs(in_fd)) + errno = ENOSYS; /* Use fallback. */ + break; + case ENOSYS: + uv__store_relaxed(&no_copy_file_range_support, 1); + break; + case EPERM: + /* It's been reported that CIFS spuriously fails. + * Consider it a transient error. + */ + if (uv__is_cifs_or_smb(out_fd)) + errno = ENOSYS; /* Use fallback. */ + break; + case ENOTSUP: + case EXDEV: + /* ENOTSUP - it could work on another file system type. + * EXDEV - it will not work when in_fd and out_fd are not on the same + * mounted filesystem (pre Linux 5.3) + */ + errno = ENOSYS; /* Use fallback. */ + break; + } + + return -1; +} + #endif /* __linux__ */ @@ -960,40 +1025,21 @@ static ssize_t uv__fs_sendfile(uv_fs_t* req) { { off_t off; ssize_t r; + size_t len; + int try_sendfile; off = req->off; + len = req->bufsml[0].len; + try_sendfile = 1; #ifdef __linux__ - { - static int no_copy_file_range_support; - - if (uv__load_relaxed(&no_copy_file_range_support) == 0) { - r = uv__fs_copy_file_range(in_fd, &off, out_fd, NULL, req->bufsml[0].len, 0); - - if (r == -1 && errno == ENOSYS) { - /* ENOSYS - it will never work */ - errno = 0; - uv__store_relaxed(&no_copy_file_range_support, 1); - } else if (r == -1 && errno == EACCES && uv__is_buggy_cephfs(in_fd)) { - /* EACCES - pre-4.20 kernels have a bug where CephFS uses the RADOS - copy-from command when it shouldn't */ - errno = 0; - uv__store_relaxed(&no_copy_file_range_support, 1); - } else if (r == -1 && (errno == ENOTSUP || errno == EXDEV)) { - /* ENOTSUP - it could work on another file system type */ - /* EXDEV - it will not work when in_fd and out_fd are not on the same - mounted filesystem (pre Linux 5.3) */ - errno = 0; - } else { - goto ok; - } - } - } + r = uv__fs_try_copy_file_range(in_fd, &off, out_fd, len); + try_sendfile = (r == -1 && errno == ENOSYS); #endif - r = sendfile(out_fd, in_fd, &off, req->bufsml[0].len); + if (try_sendfile) + r = sendfile(out_fd, in_fd, &off, len); -ok: /* sendfile() on SunOS returns EINVAL if the target fd is not a socket but * it still writes out data. Fortunately, we can detect it by checking if * the offset has been updated. @@ -1277,22 +1323,15 @@ static ssize_t uv__fs_copyfile(uv_fs_t* req) { if (fchmod(dstfd, src_statsbuf.st_mode) == -1) { err = UV__ERR(errno); #ifdef __linux__ + /* fchmod() on CIFS shares always fails with EPERM unless the share is + * mounted with "noperm". As fchmod() is a meaningless operation on such + * shares anyway, detect that condition and squelch the error. + */ if (err != UV_EPERM) goto out; - { - struct statfs s; - - /* fchmod() on CIFS shares always fails with EPERM unless the share is - * mounted with "noperm". As fchmod() is a meaningless operation on such - * shares anyway, detect that condition and squelch the error. - */ - if (fstatfs(dstfd, &s) == -1) - goto out; - - if ((unsigned) s.f_type != /* CIFS */ 0xFF534D42u) - goto out; - } + if (!uv__is_cifs_or_smb(dstfd)) + goto out; err = 0; #else /* !__linux__ */