Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 29 additions & 4 deletions sshfs.c
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@
# define MAP_ANONYMOUS MAP_ANON
#endif

#ifndef RENAME_NOREPLACE
# define RENAME_NOREPLACE (1 << 0)
#endif


#define SSH_FXP_INIT 1
#define SSH_FXP_VERSION 2
Expand Down Expand Up @@ -2708,15 +2712,35 @@ static int sshfs_rename(const char *from, const char *to, unsigned int flags)
{
int err;
struct conntab_entry *ce;
int noreplace = flags & RENAME_NOREPLACE;

if(flags != 0)
if (flags & ~RENAME_NOREPLACE)
return -EINVAL;

if (sshfs.ext_posix_rename)
if (noreplace && sshfs.follow_symlinks) {
struct buffer buf;
struct buffer outbuf;

/* getattr follows symlinks, so the kernel misses a dangling target */
buf_init(&buf, 0);
buf_add_path(&buf, to);
err = sftp_request(get_conn(NULL, NULL), SSH_FXP_LSTAT, &buf,
SSH_FXP_ATTRS, &outbuf);
buf_free(&buf);
if (!err) {
buf_free(&outbuf);
return -EEXIST;
}
if (err != -ENOENT)
return err;
}

/* posix-rename and the rename workaround both replace the target */
if (sshfs.ext_posix_rename && !noreplace)
err = sshfs_ext_posix_rename(from, to);
else
err = sshfs_do_rename(from, to);
if (err == -EPERM && sshfs.rename_workaround) {
if (err == -EPERM && sshfs.rename_workaround && !noreplace) {
size_t tolen = strlen(to);
if (tolen + RENAME_TEMP_CHARS < PATH_MAX) {
int tmperr;
Expand All @@ -2733,7 +2757,8 @@ static int sshfs_rename(const char *from, const char *to, unsigned int flags)
}
}
}
if (err == -EPERM && sshfs.renamexdev_workaround)
/* EXDEV would make mv fall back to copying over the target */
if (err == -EPERM && sshfs.renamexdev_workaround && !noreplace)
err = -EXDEV;

if (!err && sshfs.max_conns > 1) {
Expand Down
48 changes: 48 additions & 0 deletions test/test_sshfs.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
sys.exit(pytest.main([__file__] + sys.argv[1:]))

import subprocess
import ctypes
import os
import sys
import pytest
Expand Down Expand Up @@ -151,6 +152,7 @@ def test_sshfs(
tst_rmdir(src_dir, mnt_dir, cache_timeout)
tst_rename(mnt_dir)
tst_rename_over(mnt_dir)
tst_rename_noreplace(mnt_dir)
tst_chmod(mnt_dir)
tst_fsync(src_dir, mnt_dir)
tst_unlink(src_dir, mnt_dir, cache_timeout)
Expand Down Expand Up @@ -264,6 +266,22 @@ def tst_rename_over(mnt_dir):
os.unlink(dst_name)


def tst_rename_noreplace(mnt_dir):
# macFUSE's FSKit backend sets RENAME_NOREPLACE on every rename to a new
# name. The kernel rejects a target it can see, so that is not tested here.
libc = ctypes.CDLL(None, use_errno=True)
src, dst = pjoin(mnt_dir, name_generator()), pjoin(mnt_dir, name_generator())
with open(src, "wb") as fh:
fh.write(b"old\n")
# AT_FDCWD, RENAME_NOREPLACE
if libc.renameat2(-100, os.fsencode(src), -100, os.fsencode(dst), 1):
raise OSError(ctypes.get_errno(), "renameat2")
assert not os.path.exists(src)
with open(dst, "rb") as fh:
assert fh.read() == b"old\n"
os.unlink(dst)


def tst_chmod(mnt_dir):
filename = pjoin(mnt_dir, name_generator())
with open(filename, "wb") as fh:
Expand Down Expand Up @@ -866,6 +884,36 @@ def test_follow_symlinks(tmpdir, capfd):
with open(mnt_link, "rb") as fh:
assert fh.read() == b"symlink target data"

# A dangling symlink looks absent through follow_symlinks, so the
# kernel lets RENAME_NOREPLACE through and sshfs must refuse it.
libc = ctypes.CDLL(None, use_errno=True)
dangling = name_generator()
os.symlink("missing", pjoin(src_dir, dangling))
with pytest.raises(FileNotFoundError):
os.lstat(pjoin(mnt_dir, dangling))
# AT_FDCWD, RENAME_NOREPLACE
ret = libc.renameat2(
-100, os.fsencode(mnt_link), -100, os.fsencode(pjoin(mnt_dir, dangling)), 1
)
assert ret == -1 and ctypes.get_errno() == errno.EEXIST
assert os.readlink(pjoin(src_dir, dangling)) == "missing"
assert os.readlink(link) == target_name
os.unlink(pjoin(src_dir, dangling))

# A genuinely absent target is still renamed
fresh, moved = name_generator(), name_generator()
os.symlink(target_name, pjoin(src_dir, fresh))
ret = libc.renameat2(
-100,
os.fsencode(pjoin(mnt_dir, fresh)),
-100,
os.fsencode(pjoin(mnt_dir, moved)),
1,
)
assert ret == 0
assert os.readlink(pjoin(src_dir, moved)) == target_name
os.unlink(pjoin(src_dir, moved))

os.unlink(link)
os.unlink(target)
except Exception:
Expand Down
Loading