diff --git a/openssh-9.9p1-canonical-match-user.patch b/openssh-9.9p1-canonical-match-user.patch
new file mode 100644
index 0000000..1adf688
--- /dev/null
+++ b/openssh-9.9p1-canonical-match-user.patch
@@ -0,0 +1,120 @@
+diff --color -ruNp a/auth.c b/auth.c
+--- a/auth.c 2025-10-29 09:57:20.440640281 +0100
++++ b/auth.c 2025-10-29 09:57:54.747956792 +0100
+@@ -476,8 +476,12 @@ getpwnamallow(struct ssh *ssh, const cha
+ u_int i;
+
+ ci = server_get_connection_info(ssh, 1, options.use_dns);
+- ci->user = user;
+- ci->user_invalid = getpwnam(user) == NULL;
++ pw = getpwnam(user);
++ if (pw != NULL && options.canonical_match_user)
++ ci->user = pw->pw_name;
++ else
++ ci->user = user;
++ ci->user_invalid = pw == NULL;
+ parse_server_match_config(&options, &includes, ci);
+ log_change_level(options.log_level);
+ log_verbose_reset();
+diff --color -ruNp a/servconf.c b/servconf.c
+--- a/servconf.c 2025-10-29 09:57:20.502465586 +0100
++++ b/servconf.c 2025-10-29 09:57:54.748205464 +0100
+@@ -225,6 +225,7 @@ initialize_server_options(ServerOptions
+ options->unused_connection_timeout = -1;
+ options->sshd_session_path = NULL;
+ options->refuse_connection = -1;
++ options->canonical_match_user = -1;
+ }
+
+ /* Returns 1 if a string option is unset or set to "none" or 0 otherwise. */
+@@ -526,6 +527,8 @@ fill_default_server_options(ServerOption
+ options->sshd_session_path = xstrdup(_PATH_SSHD_SESSION);
+ if (options->refuse_connection == -1)
+ options->refuse_connection = 0;
++ if (options->canonical_match_user == -1)
++ options->canonical_match_user = 0;
+
+ assemble_algorithms(options);
+
+@@ -609,7 +612,7 @@ typedef enum {
+ sAllowStreamLocalForwarding, sFingerprintHash, sDisableForwarding,
+ sExposeAuthInfo, sRDomain, sPubkeyAuthOptions, sSecurityKeyProvider,
+ sRequiredRSASize, sChannelTimeout, sUnusedConnectionTimeout,
+- sSshdSessionPath, sRefuseConnection,
++ sSshdSessionPath, sRefuseConnection, sCanonicalMatchUser,
+ sDeprecated, sIgnore, sUnsupported
+ } ServerOpCodes;
+
+@@ -798,6 +801,7 @@ static struct {
+ { "unusedconnectiontimeout", sUnusedConnectionTimeout, SSHCFG_ALL },
+ { "sshdsessionpath", sSshdSessionPath, SSHCFG_GLOBAL },
+ { "refuseconnection", sRefuseConnection, SSHCFG_ALL },
++ { "canonicalmatchuser", sCanonicalMatchUser, SSHCFG_GLOBAL },
+ { NULL, sBadOption, 0 }
+ };
+
+@@ -2807,6 +2811,11 @@ process_server_config_line_depth(ServerO
+ multistate_ptr = multistate_flag;
+ goto parse_multistate;
+
++ case sCanonicalMatchUser:
++ intptr = &options->canonical_match_user;
++ multistate_ptr = multistate_flag;
++ goto parse_multistate;
++
+ case sDeprecated:
+ case sIgnore:
+ case sUnsupported:
+@@ -3028,6 +3037,7 @@ copy_set_server_options(ServerOptions *d
+ M_CP_INTOPT(required_rsa_size);
+ M_CP_INTOPT(unused_connection_timeout);
+ M_CP_INTOPT(refuse_connection);
++ M_CP_INTOPT(canonical_match_user);
+
+ /*
+ * The bind_mask is a mode_t that may be unsigned, so we can't use
+@@ -3368,6 +3378,7 @@ dump_config(ServerOptions *o)
+ dump_cfg_fmtint(sFingerprintHash, o->fingerprint_hash);
+ dump_cfg_fmtint(sExposeAuthInfo, o->expose_userauth_info);
+ dump_cfg_fmtint(sRefuseConnection, o->refuse_connection);
++ dump_cfg_fmtint(sCanonicalMatchUser, o->canonical_match_user);
+
+ /* string arguments */
+ dump_cfg_string(sPidFile, o->pid_file);
+diff --color -ruNp a/servconf.h b/servconf.h
+--- a/servconf.h 2025-10-29 09:57:20.503199202 +0100
++++ b/servconf.h 2025-10-29 09:57:54.749236422 +0100
+@@ -261,6 +261,8 @@ typedef struct {
+ char *sshd_session_path;
+
+ int refuse_connection;
++
++ int canonical_match_user;
+ } ServerOptions;
+
+ /* Information about the incoming connection as used by Match */
+diff --color -ruNp a/sshd_config.5 b/sshd_config.5
+--- a/sshd_config.5 2025-10-29 09:57:20.503465608 +0100
++++ b/sshd_config.5 2025-10-29 09:57:54.749544972 +0100
+@@ -1384,6 +1384,7 @@ Available keywords are
+ .Cm PubkeyAuthentication ,
+ .Cm PubkeyAuthOptions ,
+ .Cm RefuseConnection ,
++.Cm CanonicalMatchUser ,
+ .Cm RekeyLimit ,
+ .Cm RevokedKeys ,
+ .Cm RDomain ,
+@@ -1828,6 +1829,13 @@ are enabled.
+ This option is only really useful in a
+ .Cm Match
+ block.
++.It Cm CanonicalMatchUser
++Some password databases allow users to define aliases for their username.
++This directive indicates that
++.Xr sshd 8
++should attempt to first obtain a canonical username from a password database before evaluating a
++.Cm Match User
++conditional block.
+ .It Cm RekeyLimit
+ Specifies the maximum amount of data that may be transmitted or received
+ before the session key is renegotiated, optionally followed by a maximum
diff --git a/openssh-9.9p1-scp-traversing.patch b/openssh-9.9p1-scp-traversing.patch
new file mode 100644
index 0000000..2378b3a
--- /dev/null
+++ b/openssh-9.9p1-scp-traversing.patch
@@ -0,0 +1,437 @@
+diff --color -ruNp a/regress/scp3.sh b/regress/scp3.sh
+--- a/regress/scp3.sh 2024-09-20 00:20:48.000000000 +0200
++++ b/regress/scp3.sh 2025-10-29 16:00:59.815068193 +0100
+@@ -6,6 +6,12 @@ tid="scp3"
+ COPY2=${OBJ}/copy2
+ DIR=${COPY}.dd
+ DIR2=${COPY}.dd2
++DIFFOPT="-rN"
++
++# Figure out if diff does not understand "-N"
++if ! diff -N ${SRC}/scp.sh ${SRC}/scp.sh 2>/dev/null; then
++ DIFFOPT="-r"
++fi
+
+ maybe_add_scp_path_to_sshd
+
+@@ -63,6 +69,15 @@ for mode in scp sftp ; do
+ echo b > ${COPY2}
+ $SCP $scpopts -3 hostA:${DATA} hostA:${COPY} hostB:${COPY2}
+ cmp ${COPY} ${COPY2} >/dev/null && fail "corrupt target"
++
++ # scp /blah/.. is only supported via the sftp protocol.
++ # Original protocol scp just refuses it.
++ test $mode != sftp && continue
++ verbose "$tag: recursive .."
++ forest
++ $SCP $scpopts -r hostA:${DIR}/subdir/.. hostB:${DIR2} || \
++ fail "copy failed"
++ diff ${DIFFOPT} ${DIR} ${DIR2} || fail "corrupted copy"
+ done
+
+ scpclean
+diff --color -ruNp a/regress/scp.sh b/regress/scp.sh
+--- a/regress/scp.sh 2024-09-20 00:20:48.000000000 +0200
++++ b/regress/scp.sh 2025-10-29 16:00:59.810765653 +0100
+@@ -199,6 +199,19 @@ for mode in scp sftp ; do
+ echo b > ${COPY2}
+ $SCP $scpopts ${DATA} ${COPY} ${COPY2}
+ cmp ${COPY} ${COPY2} >/dev/null && fail "corrupt target"
++
++ # scp /blah/.. is only supported via the sftp protocol.
++ # Original protocol scp just refuses it.
++ test $mode != sftp && continue
++ verbose "$tag: recursive local .. to remote dir"
++ forest
++ $SCP $scpopts -r ${DIR}/subdir/.. somehost:${DIR2} || fail "copy failed"
++ diff ${DIFFOPT} ${DIR} ${DIR2} || fail "corrupted copy"
++
++ verbose "$tag: recursive remote .. to local dir"
++ forest
++ $SCP $scpopts -r somehost:${DIR}/subdir/.. ${DIR2} || fail "copy failed"
++ diff ${DIFFOPT} ${DIR} ${DIR2} || fail "corrupted copy"
+ done
+
+ scpclean
+diff --color -ruNp a/regress/sftp-cmds.sh b/regress/sftp-cmds.sh
+--- a/regress/sftp-cmds.sh 2024-09-20 00:20:48.000000000 +0200
++++ b/regress/sftp-cmds.sh 2025-10-29 16:00:59.813392901 +0100
+@@ -7,6 +7,12 @@
+
+ tid="sftp commands"
+
++DIFFOPT="-rN"
++# Figure out if diff does not understand "-N"
++if ! diff -N ${SRC}/sftp-cmds.sh ${SRC}/sftp-cmds.sh 2>/dev/null; then
++ DIFFOPT="-r"
++fi
++
+ # test that these files are readable!
+ for i in `(cd /bin;echo l*)`
+ do
+@@ -24,207 +30,246 @@ SPACECOPY_ARG="${COPY}\ this\ has\ space
+ # File with glob metacharacters
+ GLOBMETACOPY="${COPY} [metachar].txt"
+
++sftpserver() {
++ ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1
++}
++
++sftpserver_with_stdout() {
++ ${SFTP} -D ${SFTPSERVER} 2>&1
++}
++
++forest() {
++ rm -rf ${COPY}.dd/*
++ rm -rf ${COPY}.dd2
++ mkdir -p ${COPY}.dd/a ${COPY}.dd/b ${COPY}.dd/c ${COPY}.dd/a/d
++ echo 'A' > ${COPY}.dd/a/A
++ echo 'B' > ${COPY}.dd/a/B
++ echo 'C' > ${COPY}.dd/a/C
++ echo 'D' > ${COPY}.dd/a/D
++}
++
+ rm -rf ${COPY} ${COPY}.1 ${COPY}.2 ${COPY}.dd ${COPY}.dd2
+ mkdir ${COPY}.dd
+
+ verbose "$tid: lls"
+-printf "lcd ${OBJ}\nlls\n" | ${SFTP} -D ${SFTPSERVER} 2>&1 | \
++printf "lcd ${OBJ}\nlls\n" | sftpserver_with_stdout | \
+ grep copy.dd >/dev/null || fail "lls failed"
+
+ verbose "$tid: lls w/path"
+-echo "lls ${OBJ}" | ${SFTP} -D ${SFTPSERVER} 2>&1 | \
++echo "lls ${OBJ}" | sftpserver_with_stdout | \
+ grep copy.dd >/dev/null || fail "lls w/path failed"
+
+ verbose "$tid: ls"
+-echo "ls ${OBJ}" | ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 \
+- || fail "ls failed"
++echo "ls ${OBJ}" | sftpserver || fail "ls failed"
+ # XXX always successful
+
+ verbose "$tid: shell"
+-echo "!echo hi there" | ${SFTP} -D ${SFTPSERVER} 2>&1 | \
++echo "!echo hi there" | sftpserver_with_stdout | \
+ egrep '^hi there$' >/dev/null || fail "shell failed"
+
+ verbose "$tid: pwd"
+-echo "pwd" | ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 \
+- || fail "pwd failed"
++echo "pwd" | sftpserver || fail "pwd failed"
+ # XXX always successful
+
+ verbose "$tid: lpwd"
+-echo "lpwd" | ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 \
+- || fail "lpwd failed"
++echo "lpwd" | sftpserver || fail "lpwd failed"
+ # XXX always successful
+
+ verbose "$tid: quit"
+-echo "quit" | ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 \
+- || fail "quit failed"
++echo "quit" | sftpserver || fail "quit failed"
+ # XXX always successful
+
+ verbose "$tid: help"
+-echo "help" | ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 \
+- || fail "help failed"
++echo "help" | sftpserver || fail "help failed"
+ # XXX always successful
+
+ rm -f ${COPY}
+ verbose "$tid: get"
+-echo "get $DATA $COPY" | ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 \
+- || fail "get failed"
++echo "get $DATA $COPY" | sftpserver || fail "get failed"
+ cmp $DATA ${COPY} || fail "corrupted copy after get"
+
+ rm -f ${COPY}
+ verbose "$tid: get quoted"
+-echo "get \"$DATA\" $COPY" | ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 \
+- || fail "get failed"
++echo "get \"$DATA\" $COPY" | sftpserver || fail "get failed"
+ cmp $DATA ${COPY} || fail "corrupted copy after get"
+
+ rm -f ${QUOTECOPY}
+ cp $DATA ${QUOTECOPY}
+ verbose "$tid: get filename with quotes"
+-echo "get \"$QUOTECOPY_ARG\" ${COPY}" | ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 \
+- || fail "get failed"
++echo "get \"$QUOTECOPY_ARG\" ${COPY}" | sftpserver || fail "get failed"
+ cmp ${COPY} ${QUOTECOPY} || fail "corrupted copy after get with quotes"
+ rm -f ${QUOTECOPY} ${COPY}
+
+ rm -f "$SPACECOPY" ${COPY}
+ cp $DATA "$SPACECOPY"
+ verbose "$tid: get filename with spaces"
+-echo "get ${SPACECOPY_ARG} ${COPY}" | ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 \
+- || fail "get failed"
++echo "get ${SPACECOPY_ARG} ${COPY}" | sftpserver || fail "get failed"
+ cmp ${COPY} "$SPACECOPY" || fail "corrupted copy after get with spaces"
+
+ rm -f "$GLOBMETACOPY" ${COPY}
+ cp $DATA "$GLOBMETACOPY"
+ verbose "$tid: get filename with glob metacharacters"
+-echo "get \"${GLOBMETACOPY}\" ${COPY}" | \
+- ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 || fail "get failed"
++echo "get \"${GLOBMETACOPY}\" ${COPY}" | sftpserver || fail "get failed"
+ cmp ${COPY} "$GLOBMETACOPY" || \
+ fail "corrupted copy after get with glob metacharacters"
+
+-rm -f ${COPY}.dd/*
++rm -rf ${COPY}.dd/*
+ verbose "$tid: get to directory"
+-echo "get $DATA ${COPY}.dd" | ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 \
+- || fail "get failed"
++echo "get $DATA ${COPY}.dd" | sftpserver || fail "get failed"
+ cmp $DATA ${COPY}.dd/${DATANAME} || fail "corrupted copy after get"
+
+-rm -f ${COPY}.dd/*
++rm -rf ${COPY}.dd/*
+ verbose "$tid: glob get to directory"
+-echo "get /bin/l* ${COPY}.dd" | ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 \
+- || fail "get failed"
++echo "get /bin/l* ${COPY}.dd" | sftpserver || fail "get failed"
+ for x in $GLOBFILES; do
+ cmp /bin/$x ${COPY}.dd/$x || fail "corrupted copy after get"
+ done
+
+-rm -f ${COPY}.dd/*
++rm -rf ${COPY}.dd/*
+ verbose "$tid: get to local dir"
+-printf "lcd ${COPY}.dd\nget $DATA\n" | ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 \
+- || fail "get failed"
++printf "lcd ${COPY}.dd\nget $DATA\n" | sftpserver || fail "get failed"
+ cmp $DATA ${COPY}.dd/${DATANAME} || fail "corrupted copy after get"
+
+-rm -f ${COPY}.dd/*
++rm -rf ${COPY}.dd/*
+ verbose "$tid: glob get to local dir"
+-printf "lcd ${COPY}.dd\nget /bin/l*\n" | ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 \
+- || fail "get failed"
++printf "lcd ${COPY}.dd\nget /bin/l*\n" | sftpserver || fail "get failed"
+ for x in $GLOBFILES; do
+ cmp /bin/$x ${COPY}.dd/$x || fail "corrupted copy after get"
+ done
+
++forest
++verbose "$tid: get recursive absolute"
++echo "get -R ${COPY}.dd ${COPY}.dd2" | sftpserver || fail "get failed"
++diff ${DIFFOPT} ${COPY}.dd ${COPY}.dd2 || fail "corrupted copy"
++
++forest
++verbose "$tid: get recursive relative src"
++printf "cd ${COPY}.dd\n get -R . ${COPY}.dd2\n" | sftpserver || \
++ fail "get failed"
++diff ${DIFFOPT} ${COPY}.dd ${COPY}.dd2 || fail "corrupted copy"
++
++forest
++verbose "$tid: get relative .."
++printf "cd ${COPY}.dd/b\n get -R .. ${COPY}.dd2\n" | sftpserver || \
++ fail "get failed"
++diff ${DIFFOPT} ${COPY}.dd ${COPY}.dd2 || fail "corrupted copy"
++
++forest
++mkdir ${COPY}.dd2
++verbose "$tid: get recursive relative .."
++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"
++
+ rm -f ${COPY}
+ verbose "$tid: put"
+-echo "put $DATA $COPY" | \
+- ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 || fail "put failed"
++echo "put $DATA $COPY" | sftpserver || fail "put failed"
+ cmp $DATA ${COPY} || fail "corrupted copy after put"
+
+ rm -f ${QUOTECOPY}
+ verbose "$tid: put filename with quotes"
+-echo "put $DATA \"$QUOTECOPY_ARG\"" | \
+- ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 || fail "put failed"
++echo "put $DATA \"$QUOTECOPY_ARG\"" | sftpserver || fail "put failed"
+ cmp $DATA ${QUOTECOPY} || fail "corrupted copy after put with quotes"
+
+ rm -f "$SPACECOPY"
+ verbose "$tid: put filename with spaces"
+-echo "put $DATA ${SPACECOPY_ARG}" | \
+- ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 || fail "put failed"
++echo "put $DATA ${SPACECOPY_ARG}" | sftpserver || fail "put failed"
+ cmp $DATA "$SPACECOPY" || fail "corrupted copy after put with spaces"
+
+-rm -f ${COPY}.dd/*
++rm -rf ${COPY}.dd/*
+ verbose "$tid: put to directory"
+-echo "put $DATA ${COPY}.dd" | ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 \
+- || fail "put failed"
++echo "put $DATA ${COPY}.dd" | sftpserver || fail "put failed"
+ cmp $DATA ${COPY}.dd/${DATANAME} || fail "corrupted copy after put"
+
+-rm -f ${COPY}.dd/*
++rm -rf ${COPY}.dd/*
+ verbose "$tid: glob put to directory"
+-echo "put /bin/l? ${COPY}.dd" | ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 \
+- || fail "put failed"
++echo "put /bin/l? ${COPY}.dd" | sftpserver || fail "put failed"
+ for x in $GLOBFILES; do
+ cmp /bin/$x ${COPY}.dd/$x || fail "corrupted copy after put"
+ done
+
+-rm -f ${COPY}.dd/*
++rm -rf ${COPY}.dd/*
+ verbose "$tid: put to local dir"
+-printf "cd ${COPY}.dd\nput $DATA\n" | ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 \
+- || fail "put failed"
++printf "cd ${COPY}.dd\nput $DATA\n" | sftpserver || fail "put failed"
+ cmp $DATA ${COPY}.dd/${DATANAME} || fail "corrupted copy after put"
+
+-rm -f ${COPY}.dd/*
++rm -rf ${COPY}.dd/*
+ verbose "$tid: glob put to local dir"
+-printf "cd ${COPY}.dd\nput /bin/l*\n" | ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 \
+- || fail "put failed"
++printf "cd ${COPY}.dd\nput /bin/l*\n" | sftpserver || fail "put failed"
+ for x in $GLOBFILES; do
+ cmp /bin/$x ${COPY}.dd/$x || fail "corrupted copy after put"
+ done
+
++forest
++verbose "$tid: put recursive absolute"
++echo "put -R ${COPY}.dd ${COPY}.dd2" | sftpserver || fail "put failed"
++diff ${DIFFOPT} ${COPY}.dd ${COPY}.dd2 || fail "corrupted copy"
++
++forest
++verbose "$tid: put recursive relative src"
++printf "lcd ${COPY}.dd\n put -R . ${COPY}.dd2\n" | sftpserver || \
++ fail "put failed"
++diff ${DIFFOPT} ${COPY}.dd ${COPY}.dd2 || fail "corrupted copy"
++
++forest
++verbose "$tid: put recursive .."
++printf "lcd ${COPY}.dd/b\n put -R .. ${COPY}.dd2\n" | sftpserver || \
++ fail "put failed"
++diff ${DIFFOPT} ${COPY}.dd ${COPY}.dd2 || fail "corrupted copy"
++
++forest
++mkdir ${COPY}.dd2
++verbose "$tid: put recursive .. relative"
++printf "lcd ${COPY}.dd/b\n cd ${COPY}.dd2\n put -R ..\n" | sftpserver || \
++ fail "put failed"
++diff ${DIFFOPT} ${COPY}.dd ${COPY}.dd2 || fail "corrupted copy"
++
+ verbose "$tid: rename"
+-echo "rename $COPY ${COPY}.1" | ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 \
+- || fail "rename failed"
++echo "rename $COPY ${COPY}.1" | sftpserver || fail "rename failed"
+ test -f ${COPY}.1 || fail "missing file after rename"
+ cmp $DATA ${COPY}.1 >/dev/null 2>&1 || fail "corrupted copy after rename"
+
+ verbose "$tid: rename directory"
+-echo "rename ${COPY}.dd ${COPY}.dd2" | \
+- ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 || \
++rm -rf ${COPY}.dd2
++echo "rename ${COPY}.dd ${COPY}.dd2" | sftpserver || \
+ fail "rename directory failed"
+ test -d ${COPY}.dd && fail "oldname exists after rename directory"
+ test -d ${COPY}.dd2 || fail "missing newname after rename directory"
+
+ verbose "$tid: ln"
+-echo "ln ${COPY}.1 ${COPY}.2" | ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 || fail "ln failed"
++echo "ln ${COPY}.1 ${COPY}.2" | sftpserver || fail "ln failed"
+ test -f ${COPY}.2 || fail "missing file after ln"
+ cmp ${COPY}.1 ${COPY}.2 || fail "created file is not equal after ln"
+
+ verbose "$tid: ln -s"
+ rm -f ${COPY}.2
+-echo "ln -s ${COPY}.1 ${COPY}.2" | ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 || fail "ln -s failed"
++echo "ln -s ${COPY}.1 ${COPY}.2" | sftpserver || fail "ln -s failed"
+ test -h ${COPY}.2 || fail "missing file after ln -s"
+
+ verbose "$tid: cp"
+ rm -f ${COPY}.2
+-echo "cp ${COPY}.1 ${COPY}.2" | ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 || fail "cp failed"
++echo "cp ${COPY}.1 ${COPY}.2" | sftpserver || fail "cp failed"
+ cmp ${COPY}.1 ${COPY}.2 || fail "created file is not equal after cp"
+
+ verbose "$tid: mkdir"
+-echo "mkdir ${COPY}.dd" | ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 \
+- || fail "mkdir failed"
++echo "mkdir ${COPY}.dd" | sftpserver || fail "mkdir failed"
+ test -d ${COPY}.dd || fail "missing directory after mkdir"
+
+ # XXX do more here
+ verbose "$tid: chdir"
+-echo "chdir ${COPY}.dd" | ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 \
+- || fail "chdir failed"
++echo "chdir ${COPY}.dd" | sftpserver || fail "chdir failed"
+
+ verbose "$tid: rmdir"
+-echo "rmdir ${COPY}.dd" | ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 \
+- || fail "rmdir failed"
++echo "rmdir ${COPY}.dd" | sftpserver || fail "rmdir failed"
+ test -d ${COPY}.1 && fail "present directory after rmdir"
+
+ verbose "$tid: lmkdir"
+-echo "lmkdir ${COPY}.dd" | ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 \
+- || fail "lmkdir failed"
++echo "lmkdir ${COPY}.dd" | sftpserver || fail "lmkdir failed"
+ test -d ${COPY}.dd || fail "missing directory after lmkdir"
+
+ # XXX do more here
+ verbose "$tid: lchdir"
+-echo "lchdir ${COPY}.dd" | ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 \
+- || fail "lchdir failed"
++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 --color -ruNp a/scp.c b/scp.c
+--- a/scp.c 2025-10-29 15:49:51.795813258 +0100
++++ b/scp.c 2025-10-29 16:00:59.817393000 +0100
+@@ -1362,6 +1362,10 @@ source_sftp(int argc, char *src, char *t
+ if ((filename = basename(src)) == NULL)
+ fatal("basename \"%s\": %s", src, strerror(errno));
+
++ /* Special handling for source of '..' */
++ if (strcmp(filename, "..") == 0)
++ filename = "."; /* Upload to dest, not dest/.. */
++
+ /*
+ * No need to glob here - the local shell already took care of
+ * the expansions
+@@ -1635,6 +1639,10 @@ sink_sftp(int argc, char *dst, const cha
+ goto out;
+ }
+
++ /* Special handling for destination of '..' */
++ if (strcmp(filename, "..") == 0)
++ filename = "."; /* Download to dest, not dest/.. */
++
+ if (dst_is_dir)
+ abs_dst = sftp_path_append(dst, filename);
+ else
+diff --color -ruNp a/sftp.c b/sftp.c
+--- a/sftp.c 2025-10-29 15:49:51.796907436 +0100
++++ b/sftp.c 2025-10-29 16:00:59.819803948 +0100
+@@ -687,6 +687,10 @@ process_get(struct sftp_conn *conn, cons
+ goto out;
+ }
+
++ /* Special handling for dest of '..' */
++ if (strcmp(filename, "..") == 0)
++ filename = "."; /* Download to dest, not dest/.. */
++
+ if (g.gl_matchc == 1 && dst) {
+ if (local_is_dir(dst)) {
+ abs_dst = sftp_path_append(dst, filename);
+@@ -781,6 +785,9 @@ process_put(struct sftp_conn *conn, cons
+ err = -1;
+ goto out;
+ }
++ /* Special handling for source of '..' */
++ if (strcmp(filename, "..") == 0)
++ filename = "."; /* Upload to dest, not dest/.. */
+
+ free(abs_dst);
+ abs_dst = NULL;
diff --git a/openssh.spec b/openssh.spec
index a5c0a0e..7f49c28 100644
--- a/openssh.spec
+++ b/openssh.spec
@@ -43,7 +43,7 @@
Summary: An open source implementation of SSH protocol version 2
Name: openssh
Version: %{openssh_ver}
-Release: 14%{?dist}.alma.1
+Release: 15%{?dist}.alma.1
URL: http://www.openssh.com/portable.html
Source0: ftp://ftp.openbsd.org/pub/OpenBSD/OpenSSH/portable/openssh-%{version}.tar.gz
Source1: ftp://ftp.openbsd.org/pub/OpenBSD/OpenSSH/portable/openssh-%{version}.tar.gz.asc
@@ -216,6 +216,10 @@ Patch1026: openssh-9.9p1-bad-hostkey.patch
Patch1027: openssh-9.9p1-support-authentication-indicators-in-GSSAPI.patch
#
Patch1028: openssh-9.9p1-fips-gss.patch
+#upstream 6432b9f6a216d0f5fb43df500e9bc30bebb3f58b
+#upstream 4f14ca8633a2c8c0a1a19165663421f0ab32f6ab
+Patch1029: openssh-9.9p1-scp-traversing.patch
+Patch1030: openssh-9.9p1-canonical-match-user.patch
License: BSD-3-Clause AND BSD-2-Clause AND ISC AND SSH-OpenSSH AND ssh-keyscan AND sprintf AND LicenseRef-Fedora-Public-Domain AND X11-distribute-modifications-variant
Requires: /sbin/nologin
@@ -409,6 +413,8 @@ gpgv2 --quiet --keyring %{SOURCE3} %{SOURCE1} %{SOURCE0}
%patch -P 1026 -p1 -b .bad-hostkey
%patch -P 1027 -p1 -b .gss-indicators
%patch -P 1028 -p1 -b .gss-fips
+%patch -P 1029 -p1 -b .scp-traversing
+%patch -P 1030 -p1 -b .canonical-match-user
%patch -P 100 -p1 -b .coverity
@@ -689,9 +695,15 @@ test -f %{sysconfig_anaconda} && \
%attr(0755,root,root) %{_libdir}/sshtest/sk-dummy.so
%changelog
-* Tue Sep 16 2025 Koichiro Iwao - 9.9p1-14.alma.1
+* Fri Oct 31 2025 Koichiro Iwao - 9.9p1-15.alma.1
- Unpatch Red Hat help message
+* Mon Oct 27 2025 Zoltan Fridrich - 9.9p1-15
+- Fix implicit destination path selection when source path ends with ".."
+ Resolves: RHEL-118406
+- Canonicalize username when matching a user
+ Resolves: RHEL-101440
+
* Mon Sep 15 2025 Dmitry Belyavskiy - 9.9p1-14
- Relax GSS Kex restriction in FIPS mode
Resolves: RHEL-91181