From 9fd9cddd09238002349db9b454d6d46034115548 Mon Sep 17 00:00:00 2001 From: pyth Date: Sat, 12 Sep 2026 16:41:12 +0200 Subject: [PATCH] Preserve the locally requested mode when the remote SFTP server uses a different umask --- ChangeLog.rst | 6 +++ sshfs.c | 93 ++++++++++++++++++++++++++++++---------------- test/test_sshfs.py | 60 ++++++++++++++++++++++++++++++ 3 files changed, 128 insertions(+), 31 deletions(-) mode change 100755 => 100644 test/test_sshfs.py diff --git a/ChangeLog.rst b/ChangeLog.rst index 7856ba9..7300c32 100644 --- a/ChangeLog.rst +++ b/ChangeLog.rst @@ -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) -------------------------- diff --git a/sshfs.c b/sshfs.c index 3bd4ec5..397ab85 100644 --- a/sshfs.c +++ b/sshfs.c @@ -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) { @@ -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; @@ -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) { @@ -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, @@ -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; @@ -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); @@ -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); @@ -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) @@ -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, diff --git a/test/test_sshfs.py b/test/test_sshfs.py old mode 100755 new mode 100644 index c4cc64f..82ddce2 --- a/test/test_sshfs.py +++ b/test/test_sshfs.py @@ -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."""