linux: work around CIFS EPERM bug (#3354)

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
This commit is contained in:
Ben Noordhuis 2021-11-09 22:17:50 +01:00 committed by GitHub
parent 48e0427533
commit 0f696da5f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -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__ */