Skip to content

Commit

Permalink
linux: work around CIFS EPERM bug
Browse files Browse the repository at this point in the history
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: libuv#3322
  • Loading branch information
bnoordhuis committed Nov 8, 2021
1 parent 48e0427 commit 3368d0b
Showing 1 changed file with 79 additions and 40 deletions.
119 changes: 79 additions & 40 deletions src/unix/fs.c
Original file line number Diff line number Diff line change
Expand Up @@ -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__ */


Expand All @@ -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.
Expand Down Expand Up @@ -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__ */
Expand Down

0 comments on commit 3368d0b

Please sign in to comment.