Skip to content
Open
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
6 changes: 6 additions & 0 deletions ChangeLog.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
Unreleased Changes
------------------

* Preserve the locally requested mode when the remote SFTP server uses a
different umask.

Release 3.7.6 (2026-05-30)
--------------------------

Expand Down
93 changes: 62 additions & 31 deletions sshfs.c
Original file line number Diff line number Diff line change
Expand Up @@ -2573,17 +2573,48 @@ static int sshfs_releasedir(const char *path, struct fuse_file_info *fi)
}


static int sshfs_set_mode(struct conn *conn, const char *path, const struct buffer *handle, mode_t mode)
{
int err;
struct buffer buf;

buf_init(&buf, 0);
if (handle == NULL) {
buf_add_path(&buf, path);
} else {
buf_add_buf(&buf, handle);
}
buf_add_uint32(&buf, SSH_FILEXFER_ATTR_PERMISSIONS);
buf_add_uint32(&buf, mode);
err = sftp_request(conn, handle == NULL ? SSH_FXP_SETSTAT : SSH_FXP_FSETSTAT, &buf, SSH_FXP_STATUS, NULL);
buf_free(&buf);
return err;
}

static int sshfs_unlink(const char *path);
static int sshfs_rmdir(const char *path);

static int sshfs_mkdir(const char *path, mode_t mode)
{
int err;
struct conn *conn;
struct buffer buf;

conn = get_conn(NULL, NULL);
buf_init(&buf, 0);
buf_add_path(&buf, path);
buf_add_uint32(&buf, SSH_FILEXFER_ATTR_PERMISSIONS);
buf_add_uint32(&buf, mode);
// Commutes with pending write(), so we can use any connection
err = sftp_request(get_conn(NULL, NULL), SSH_FXP_MKDIR, &buf, SSH_FXP_STATUS, NULL);
err = sftp_request(conn, SSH_FXP_MKDIR, &buf, SSH_FXP_STATUS, NULL);
buf_free(&buf);
if (!err) {
err = sshfs_set_mode(conn, path, NULL, mode);
if (err) {
sshfs_rmdir(path);
}
return err;
}

if (err == -EPERM) {
if (sshfs.op->access(path, R_OK) == 0) {
Expand Down Expand Up @@ -2615,12 +2646,16 @@ static int sshfs_mknod(const char *path, mode_t mode, dev_t rdev)
buf_add_uint32(&buf, mode);
err = sftp_request(conn, SSH_FXP_OPEN, &buf, SSH_FXP_HANDLE, &handle);
if (!err) {
int err2;
int mode_err;
int close_err;
buf_finish(&handle);
err2 = sftp_request(conn, SSH_FXP_CLOSE, &handle, SSH_FXP_STATUS, NULL);
if (!err)
err = err2;
mode_err = sshfs_set_mode(conn, NULL, &handle, mode);
close_err = sftp_request(conn, SSH_FXP_CLOSE, &handle, SSH_FXP_STATUS, NULL);
err = mode_err ? mode_err : close_err;
buf_free(&handle);
if (mode_err) {
sshfs_unlink(path);
}
}
buf_free(&buf);
return err;
Expand Down Expand Up @@ -2789,8 +2824,6 @@ static int sshfs_chmod(const char *path, mode_t mode,
struct fuse_file_info *fi)
{
(void) fi;
int err;
struct buffer buf;
struct sshfs_file *sf = NULL;

if (fi != NULL) {
Expand All @@ -2799,23 +2832,10 @@ static int sshfs_chmod(const char *path, mode_t mode,
return -EIO;
}

buf_init(&buf, 0);
if (sf == NULL)
buf_add_path(&buf, path);
else
buf_add_buf(&buf, &sf->handle);

buf_add_uint32(&buf, SSH_FILEXFER_ATTR_PERMISSIONS);
buf_add_uint32(&buf, mode);

/* FIXME: really needs LSETSTAT extension (debian Bug#640038) */
// Commutes with pending write(), so we can use any connection
// if the file is not open.
err = sftp_request(get_conn(sf, NULL),
sf == NULL ? SSH_FXP_SETSTAT : SSH_FXP_FSETSTAT,
&buf, SSH_FXP_STATUS, NULL);
buf_free(&buf);
return err;
return sshfs_set_mode(get_conn(sf, NULL), sf == NULL ? path : NULL, sf == NULL ? NULL : &sf->handle, mode);
}

static int sshfs_chown(const char *path, uid_t uid, gid_t gid,
Expand Down Expand Up @@ -2917,11 +2937,11 @@ static gboolean conntab_entry_is(gpointer key, gpointer value, gpointer data)
return value == data;
}

static int sshfs_open_common(const char *path, mode_t mode,
struct fuse_file_info *fi)
static int sshfs_open_common(const char *path, mode_t mode, struct fuse_file_info *fi, int created)
{
int err;
int err2;
int opened;
struct buffer buf;
struct buffer outbuf;
struct stat stbuf;
Expand Down Expand Up @@ -2991,7 +3011,7 @@ static int sshfs_open_common(const char *path, mode_t mode,
buf_add_path(&buf, path);
buf_add_uint32(&buf, pflags);
buf_add_uint32(&buf, SSH_FILEXFER_ATTR_PERMISSIONS);
buf_add_uint32(&buf, mode);
buf_add_uint32(&buf, created && sshfs.createmode_workaround ? 0 : mode);
buf_to_iov(&buf, &iov);
sftp_request_send(sf->conn, SSH_FXP_OPEN, &iov, 1, NULL, NULL, 1, NULL,
&open_req);
Expand All @@ -3005,14 +3025,28 @@ static int sshfs_open_common(const char *path, mode_t mode,
}
err = sftp_request_wait(open_req, SSH_FXP_OPEN, SSH_FXP_HANDLE,
&sf->handle);
if (!err && err2) {
opened = !err;
if (opened) {
buf_finish(&sf->handle);
if (created && !err2 && ((stbuf.st_mode ^ mode) & 07777) != 0) {
err = sshfs_set_mode(sf->conn, NULL, &sf->handle, mode);
}
if (!err && err2) {
err = err2;
}
}
if (opened && err) {
sftp_request(sf->conn, SSH_FXP_CLOSE, &sf->handle, 0, NULL);
buf_free(&sf->handle);
err = err2;
if (created) {
sshfs_unlink(path);
}
}

if (!err) {
if (created) {
stbuf.st_mode = (stbuf.st_mode & S_IFMT) | (mode & ~S_IFMT);
}
if (sshfs.dir_cache)
cache_add_attr(path, &stbuf, wrctr);
buf_finish(&sf->handle);
Expand Down Expand Up @@ -3040,7 +3074,7 @@ static int sshfs_open_common(const char *path, mode_t mode,

static int sshfs_open(const char *path, struct fuse_file_info *fi)
{
return sshfs_open_common(path, 0, fi);
return sshfs_open_common(path, 0, fi, 0);
}

static int sshfs_flush(const char *path, struct fuse_file_info *fi)
Expand Down Expand Up @@ -3554,10 +3588,7 @@ static int sshfs_statfs(const char *path, struct statvfs *buf)
static int sshfs_create(const char *path, mode_t mode,
struct fuse_file_info *fi)
{
if (sshfs.createmode_workaround)
mode = 0;

return sshfs_open_common(path, mode, fi);
return sshfs_open_common(path, mode, fi, 1);
}

static int sshfs_truncate(const char *path, off_t size,
Expand Down
60 changes: 60 additions & 0 deletions test/test_sshfs.py
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -922,6 +922,66 @@ def test_bad_sftp_reply_len(tmpdir):
assert "bad reply len: 0" in res.stderr


@pytest.mark.parametrize("createmode_workaround", [False, True])
def test_create_preserves_local_umask(tmpdir, createmode_workaround):
"""The SFTP server's umask must not mask create modes a second time."""
candidates = [
shutil.which("sftp-server"),
"/usr/lib/openssh/sftp-server",
"/usr/lib/ssh/sftp-server",
"/usr/libexec/openssh/sftp-server",
"/usr/libexec/sftp-server",
]
sftp_server = next(
(path for path in candidates if path and os.access(path, os.X_OK)), None
)
if sftp_server is None:
pytest.skip("OpenSSH sftp-server not found")

helper = tmpdir.join("strict_umask_sftp.py")
helper.write(
"#!/usr/bin/env python3\n"
"import os\n"
f"os.execv({sftp_server!r}, [{sftp_server!r}, '-u', '0077'])\n"
)
helper.chmod(0o755)

mnt_dir = str(tmpdir.mkdir("mnt"))
src_dir = str(tmpdir.mkdir("src"))
cmdline = base_cmdline + [
pjoin(basename, "sshfs"),
"-f",
f"dummy:{src_dir}",
mnt_dir,
"-o", f"ssh_command={helper}",
"-o", "dir_cache=no",
"-o", "entry_timeout=0",
"-o", "attr_timeout=0",
]
if createmode_workaround:
cmdline += ["-o", "workaround=createmode"]
mount_process = subprocess.Popen(cmdline)
try:
wait_for_mount(mount_process, mnt_dir)
old_umask = os.umask(0o022)
try:
with open(pjoin(mnt_dir, "file"), "wb") as fh:
fh.write(b"data")
os.mknod(pjoin(mnt_dir, "node"), stat.S_IFREG | 0o666)
os.mkdir(pjoin(mnt_dir, "dir"), 0o777)
finally:
os.umask(old_umask)

assert stat.S_IMODE(os.stat(pjoin(src_dir, "file")).st_mode) == 0o644
assert stat.S_IMODE(os.stat(pjoin(src_dir, "node")).st_mode) == 0o644
assert stat.S_IMODE(os.stat(pjoin(src_dir, "dir")).st_mode) == 0o755
except Exception:
cleanup(mount_process, mnt_dir)
raise
else:
umount(mount_process, mnt_dir)


@contextmanager
def _sshfs_mount(src_dir, mnt_dir, extra_opts=None):
"""Mount src_dir via sshfs, yield, then unmount."""
Expand Down