[PATCH] sftp: preserve permissions of existing local directories

Muhammad Bilal meatuni001 at gmail.com
Sun Sep 20 06:27:03 AEST 2026


download_dir_internal() unconditionally applied the remote directory mode with chmod(2), including when the local destination directory already existed and -p was not requested. A remote SFTP server could therefore change the permissions of an existing local directory during a recursive download.
Track whether mkdir(2) created the destination and only apply the remote mode for newly-created directories or when permission preservation was requested. This matches the existing guards in upload_dir_internal() and crossload_dir_internal().

Add a regression test that checks recursive downloads do not change the mode of an existing local directory.


Verified with:
  make -s sftp sftp-server

  TEST_SSH_SFTP=./sftp TEST_SSH_SFTPSERVER=./sftp-server \
    sh regress/test-exec.sh ./regress ./regress/sftp-cmds.sh


The regression passes with the fix and fails against the unmodified source.
---
 regress/sftp-cmds.sh | 13 +++++++++++--
 sftp-client.c        | 17 ++++++++++-------
 2 files changed, 21 insertions(+), 9 deletions(-)

diff --git a/regress/sftp-cmds.sh b/regress/sftp-cmds.sh
index a03959d60..94b886b1b 100644
--- a/regress/sftp-cmds.sh
+++ b/regress/sftp-cmds.sh
@@ -161,6 +161,17 @@ printf "cd ${COPY}.dd/b\n lcd ${COPY}.dd2\n get -R ..\n" | sftpserver || \
 	fail "get failed"
 diff ${DIFFOPT} ${COPY}.dd ${COPY}.dd2 || fail "corrupted copy"
 
+forest
+chmod 0555 ${COPY}.dd
+mkdir ${COPY}.dd2
+chmod 0700 ${COPY}.dd2
+verbose "$tid: get recursive into existing directory preserves permissions"
+printf "get -R ${COPY}.dd ${COPY}.dd2\n" | sftpserver || \
+	fail "get failed"
+test "`(stat -c %a ${COPY}.dd2 2>/dev/null || stat -f %Lp ${COPY}.dd2)`" = \
+	700 || fail "get changed permissions of existing directory"
+chmod -R u+w ${COPY}.dd ${COPY}.dd2
+
 rm -f ${COPY}
 verbose "$tid: put"
 echo "put $DATA $COPY" | sftpserver || fail "put failed"
@@ -273,5 +284,3 @@ echo "lchdir ${COPY}.dd" | sftpserver || fail "lchdir failed"
 
 rm -rf ${COPY} ${COPY}.1 ${COPY}.2 ${COPY}.dd ${COPY}.dd2
 rm -rf ${QUOTECOPY} "$SPACECOPY" "$GLOBMETACOPY"
-
-
diff --git a/sftp-client.c b/sftp-client.c
index 1f031128e..16885b5d0 100644
--- a/sftp-client.c
+++ b/sftp-client.c
@@ -1882,7 +1882,7 @@ download_dir_internal(struct sftp_conn *conn, const char *src, const char *dst,
     int depth, Attrib *dirattrib, int preserve_flag, int print_flag,
     int resume_flag, int fsync_flag, int follow_link_flag, int inplace_flag)
 {
-	int i, ret = 0;
+	int i, ret = 0, created = 0;
 	SFTP_DIRENT **dir_entries;
 	char *filename, *new_src = NULL, *new_dst = NULL;
 	mode_t mode = 0777, tmpmode = mode;
@@ -1917,10 +1917,13 @@ download_dir_internal(struct sftp_conn *conn, const char *src, const char *dst,
 		    "did not send permissions", dst);
 	}
 
-	if (mkdir(dst, tmpmode) == -1 && errno != EEXIST) {
-		error("mkdir %s: %s", dst, strerror(errno));
-		return -1;
-	}
+	if (mkdir(dst, tmpmode) == -1) {
+		if (errno != EEXIST) {
+			error("mkdir %s: %s", dst, strerror(errno));
+			return -1;
+		}
+	} else
+		created = 1;
 
 	if (sftp_readdir(conn, src, &dir_entries) == -1) {
 		error("remote readdir \"%s\" failed", src);
@@ -1989,7 +1992,8 @@ download_dir_internal(struct sftp_conn *conn, const char *src, const char *dst,
 			    "\"%s\"", dst);
 	}
 
-	if (mode != tmpmode && chmod(dst, mode) == -1)
+	if ((created || preserve_flag) && mode != tmpmode &&
+	    chmod(dst, mode) == -1)
 		error("local chmod directory \"%s\": %s", dst,
 		    strerror(errno));
 
@@ -3010,4 +3014,3 @@ sftp_globpath_is_dir(const char *pathname)
 
 	return l > 0 && pathname[l - 1] == '/';
 }
-


More information about the openssh-unix-dev mailing list